# 一、微信小程序授权登录
# 1.1 一般的微信授权登录流程
- 用户访问第三方网站或应用,并选择使用微信授权登录
- 第三方网站或应用将用户重定向到微信登录页面
- 用户在微信登录页面上输入自己的微信账号和密码,并进行身份验证
- 用户确认是否授权第三方网站或应用访问自己的基本信息
- 如果用户授权,微信将生成一个授权凭证 (access_token)
- 微信将用户重定向回第三方网站或应用,并将授权凭证传递给该网站或应用
- 第三方网站或应用使用授权凭证来获取用户的基本信息,如昵称,头像等
- 第三方网站或应用根据获取到的用户信息进行登录后验证身份的操作,使用户可以在该网站后应用上进行相关操作
# 二、后端代码
# 2.1 controller
@RestController | |
@RequestMapping("user") | |
@AllArgsConstructor | |
public class UserController | |
{ | |
private UserService service; | |
@GetMapping("getSessionId") | |
public Result getSessionId(String code) { | |
return service.getSessionId(code); | |
} | |
@GetMapping("authLogin") | |
public Result getSessionId(@RequestBody WXAuth wxAuth) throws NoSuchPaddingException, NoSuchAlgorithmException | |
{ | |
Result result = service.authLogin(wxAuth); | |
return result; | |
} | |
} |
# 2.2 serviceImpl
@Service | |
@RequiredArgsConstructor | |
public class UserServiceImpl implements UserService | |
{ | |
@NonNull | |
private StringRedisTemplate redisTemplate; | |
@Value("${wxmini.secret}") | |
private String secret; | |
@Value("${wxmini.appid}") | |
private String appid; | |
@NonNull | |
private WxService service; | |
private UserMapper userMapper; | |
@Override | |
public Result getSessionId(String code) | |
{ | |
String url = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code"; | |
String replace = url.replace("{0}", appid).replace("{1}", secret).replace("{2}", code); | |
String res = HttpUtil.get(replace); | |
String uuid = UUID.randomUUID().toString(); | |
redisTemplate.opsForValue().set(RedisKey.WX_SESSION_ID + uuid, res, 30, TimeUnit.MINUTES); | |
Map<String, String> map = new HashMap<String, String>(); | |
map.put("sessionId", uuid); | |
return Result.SUCCESS(map); | |
} | |
@Override | |
public Result authLogin(WXAuth wxAuth) throws NoSuchPaddingException, NoSuchAlgorithmException | |
{ | |
String json = service.wxDecrypt(wxAuth.getEncryptedData(), wxAuth.getSessionId(), wxAuth.getIv()); | |
WxUserInfo wxUserInfo = JSON.parseObject(json, WxUserInfo.class); | |
String openId = wxUserInfo.getOpenId(); | |
User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getOpenId, openId).last("limit 1")); | |
UserDto userDto = new UserDto(); | |
userDto.from(wxUserInfo); | |
if (user == null) | |
{ | |
return this.register(userDto); | |
} else { | |
userDto.setId(user.getId()); | |
return this.login(userDto); | |
} | |
} | |
private Result login(UserDto userDto) | |
{ | |
String token = JWTUtils.sign(userDto.getId()); | |
userDto.setToken(token); | |
userDto.setOpenId(null); | |
userDto.setWxUnionId(null); | |
redisTemplate.opsForValue().set(RedisKey.TOKEN + token, JSON.toJSONString(userDto), 7, TimeUnit.DAYS); | |
return Result.SUCCESS(userDto); | |
} | |
private Result register(UserDto userDto) | |
{ | |
User user = new User(); | |
BeanUtils.copyProperties(userDto, user); | |
this.userMapper.insert(user); | |
userDto.setId(user.getId()); | |
return this.login(userDto); | |
} | |
} |
# 2.3 wxServiceImpl
@Service | |
@AllArgsConstructor | |
public class WxServiceImpl implements WxService | |
{ | |
private StringRedisTemplate redisTemplate; | |
public String wxDecrypt(String encryptedData, String sessionId, String vi) | |
{ | |
String json = redisTemplate.opsForValue().get(RedisKey.WX_SESSION_ID + sessionId); | |
JSONObject jsonObject = JSON.parseObject(json); | |
String sessionKey = (String) jsonObject.get("session_key"); | |
byte[] encData = Base64.decode(encryptedData); | |
byte[] iv = Base64.decode(vi); | |
byte[] key = Base64.decode(sessionKey); | |
IvParameterSpec ivSpec = new IvParameterSpec(iv); | |
try | |
{ | |
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | |
SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); | |
cipher.init(Cipher.DECRYPT_MODE,keySpec, ivSpec); | |
return new String(cipher.doFinal(encData), "UTF-8"); | |
} catch (NoSuchAlgorithmException e) | |
{ | |
throw new RuntimeException(e); | |
} catch (NoSuchPaddingException e) | |
{ | |
throw new RuntimeException(e); | |
} catch (InvalidKeyException e) | |
{ | |
throw new RuntimeException(e); | |
} catch (InvalidAlgorithmParameterException e) | |
{ | |
throw new RuntimeException(e); | |
} catch (UnsupportedEncodingException e) | |
{ | |
throw new RuntimeException(e); | |
} catch (IllegalBlockSizeException e) | |
{ | |
throw new RuntimeException(e); | |
} catch (BadPaddingException e) | |
{ | |
throw new RuntimeException(e); | |
} | |
} | |
} |
# 2.4 common
# 2.4.1 RedisKey
public class RedisKey | |
{ | |
public static final String WX_SESSION_ID = "wx_session_id"; | |
public static final String TOKEN = "token_"; | |
} |
# 2.4.2 Result
@Data | |
public class Result<T> | |
{ | |
private Integer code; | |
private String message; | |
private T data; | |
public Result(ResultCode resultCode) { | |
this.code = resultCode.getCode(); | |
this.message = resultCode.getMessage(); | |
} | |
public Result(ResultCode resultCode, T data) { | |
this.code = resultCode.getCode(); | |
this.message = resultCode.getMessage(); | |
this.data = data; | |
} | |
public Result(String message) { | |
this.message = message; | |
} | |
public static Result SUCCESS() { | |
return new Result(ResultCode.SUCCESS); | |
} | |
public static <T> Result SUCCESS(T data) { | |
return new Result(ResultCode.SUCCESS, data); | |
} | |
public static Result FAIL() { | |
return new Result(ResultCode.FAIL); | |
} | |
public static Result FAIL(String message) { | |
return new Result(message); | |
} | |
} |
# 2.5 model
# 2.5.1 WXAuth
@Data | |
public class WXAuth | |
{ | |
private String encryptedData; | |
private String iv; | |
private String sessionId; | |
} |
# 2.5.2 WxUserInfo
@Data | |
public class WxUserInfo | |
{ | |
private String openId; | |
private String nickname; | |
private String gender; | |
private String city; | |
private String province; | |
private String country; | |
private String avatarUrl; | |
private String unionId; | |
} |
# 2.6 pojo
@Data | |
@TableName("user") | |
public class User | |
{ | |
@TableId(value = "id", type = IdType.AUTO) | |
private Long id; | |
private String nickname; | |
private String wxUnionId; | |
private String portrait; | |
private String background; | |
private String phoneNumber; | |
private String openId; | |
private String username; | |
private String password; | |
private String gender; | |
} |
# 2.7 util
public class JWTUtils { | |
public static final String AUTH_HEADER_KEY = "Authorization"; | |
public static final String TOKEN_PREFIX = "Bearer "; | |
/** | |
* 过期时间一周 | |
*/ | |
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; | |
private static final String secret = "43dcbc5b-8776-429e-b122-3cae6bd97020"; | |
/** | |
* 校验 token 是否正确 | |
* | |
* @param token 密钥 | |
* @return 是否正确 | |
*/ | |
public static boolean verify(String token) { | |
try { | |
Algorithm algorithm = Algorithm.HMAC256(secret); | |
JWTVerifier verifier = JWT.require(algorithm) | |
.build(); | |
DecodedJWT jwt = verifier.verify(token); | |
return true; | |
} catch (Exception exception) { | |
return false; | |
} | |
} | |
/** | |
* 获得 token 中的信息无需 secret 解密也能获得 | |
* | |
* @return token 中包含的 id | |
*/ | |
public static Long getUserId(String token) { | |
try { | |
DecodedJWT jwt = JWT.decode(token); | |
return jwt.getClaim("id").asLong(); | |
} catch (JWTDecodeException e) { | |
return null; | |
} | |
} | |
/** | |
* 签发 token | |
* | |
* @return 加密的 token | |
*/ | |
public static String sign(Long id) { | |
try { | |
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); | |
Algorithm algorithm = Algorithm.HMAC256(secret); | |
// 附带 username 信息 | |
return JWT.create() | |
.withClaim("id", id) | |
.withExpiresAt(date) | |
.sign(algorithm); | |
} catch (UnsupportedEncodingException e) { | |
return null; | |
} | |
} | |
/** | |
* 判断过期 | |
* | |
* @param token | |
* @return | |
*/ | |
public static boolean isExpire(String token) { | |
DecodedJWT jwt = JWT.decode(token); | |
return System.currentTimeMillis() > jwt.getExpiresAt().getTime(); | |
} | |
} |
# 2.8 application.yml
wxmini: | |
appid: '' | |
secret: '' |