三步实现微信小程序登录
一、登录流程:
上面这个是微信官方给出的。在通过 wx.login() 获取到用户登录态之后,需要维护登录态。开发者要注意不应该直接把 session_key、openid 等字段作为用户的标识或者 session 的标识,而应该自己派发一个 session 登录状态。详见下面的流程图:
1.前端调用wx.login()获取code值(developers.weixin.qq.com/miniprogram... 2.前端通过调用wx.getUserInfo获取iv、rawData、signature、encryptedData等加密数据,传递给后端(developers.weixin.qq.com/miniprogram...; 3.服务器通过code请求api换回session_key和openid; 4.服务器通过前端给的rawData 加获取的session_key使用sha1加密,计算出signature1; 5.比对前端传的signature和自己算出来的signature1是否一致(防止数据不一致); 6.用AES算法解密encryptedData里的敏感数据; 7.拿着敏感数据后做自己的逻辑; 8.通知前端登陆成功;
二、首先调用wx.login获取 临时登录凭证 code
ini
wx.login({
success: res => {
// 获取到用户的 code:res.code
const code = res.code;
console.log("用户的code:" + res.code);
//传入code调用服务端接口登录
return api.userLogin.registerOrLogin({ code });
}
})
Taro 框架 是一个开放式跨端跨框架解决方案(关于taro相关文档可以参考:taro-docs.jd.com/docs/),支持使用 React/Vue/Nerv 等框架来开发应用。taro基本上做到了将微信小程序上的api都封装了,同时也紧随着小程序的更新。这个地方也可以用taro来调用:
javascript
const requestLogin = async () => {
const { code } = await Taro.login();
console.log("requestLogin code", code);
return api.userLogin.registerOrLogin({ code });
};
三、服务端后台根据code获取openId、session_key和unionId:
less
@PostMapping("/user-login/action/registerOrLogin")
public Response<LoginVO> registerOrLogin(@RequestBody UserLoginDTO userLoginDTO) {
return ResponseUtils.returnObjectSuccess(userLoginService.registerOrLoginByInfo(userLoginDTO));
}
less
public LoginVO registerOrLoginByInfo(UserLoginDTO userLoginDTO) {
UserAccount userAccount = new UserAccount();
result = authorizationGrantApi.wxMiniGrant(dto.getCode());
if (result != null) {
if (StringUtils.hasText(result.getOpenId())) {
userAccount.setOpenId(result.getOpenId());
}
if (StringUtils.hasText(result.getUnionId())) {
userAccount.setUnionId(result.getUnionId());
}
}
if (StringUtils.hasText(dto.getEncryptedData()) && StringUtils.hasText(dto.getIv())) {
ObjectNode userNode = weChatMiniGrantApi.decryptUser(dto.getEncryptedData(), result.getAuthorId(), dto.getIv());
//基本信息
if (userNode.get("openId") != null) {
userAccount.setOpenId(userNode.get("openId").textValue());
userAccount.setGender(userNode.get("gender").intValue());
userAccount.setNickName(userNode.get("nickName").textValue());
userAccount.setAvatar(userNode.get("avatarUrl").textValue());
if (userNode.get("unionId") != null) {
userAccount.setUnionId(userNode.get("unionId").textValue());
}
} else {
userAccount.setUserPhone(userNode.get("phoneNumber").textValue());
}
LoginVO vo = new LoginVO();
vo.setUserAccount(userAccount);
..........
return vo;
}
其中的authorizationGrantApi.wxMiniGrant(dto.getCode())实现了根据code获取openId和unionId:
csharp
/**
* 微信小程序授权
*
* @param header
* @param authCode
* @return
*/
public AuthorizerGrantResult wxMiniGrant(YryzRequestHeader header, String authCode) {
return this.wxMiniGrantAndDecryUser(header, authCode, null, null);
}
public AuthorizerGrantResult wxMiniGrantAndDecryUser(String authCode, String encryptedData, String iv) {
if (weChatMiniGrantApi == null) {
throw new BusinessException("", "暂未开放微信小程序授权");
}
//根据code,调用code2Session得到openid, session_key和unionId
JsonNode respNode = weChatMiniGrantApi.miniGrant(authCode);
/**
* 转换基本参数
*/
AuthorizerGrantResult base = new AuthorizerGrantResult();
base.setOpenId(respNode.get("openid").textValue());
if(respNode.get("unionid") != null){
base.setUnionId(respNode.get("unionid").textValue());
}
base.setChannelId(IAuthorizerGrantHook.WX_MINI);
/**
* 解密用户信息数据
*/
if (StringUtils.hasText(encryptedData) && StringUtils.hasText(iv)) {
ObjectNode userNode = weChatMiniGrantApi.decryptUser(encryptedData, respNode.get("session_key").textValue(), iv);
//基本信息
AuthorizerGrantResult.ChannelAuthor channelAuthor = new AuthorizerGrantResult.ChannelAuthor(userNode);
channelAuthor.setAvatar(userNode.get("avatarUrl").textValue());
channelAuthor.setGender(userNode.get("gender").intValue());
channelAuthor.setNickName(userNode.get("nickName").textValue());
channelAuthor.setPhone(userNode.get("phoneNumber").textValue());
base.setChannelAuthor(channelAuthor);
}
return authorizerGrantHook.successAfter(header, IAuthorizerGrantHook.WX_MINI, base, respNode);
}
上面代码中的wxMiniGrantAndDecryUser首先根据传入的code调用调用code2Session得到openid, session_key和unionId,然后从encryptedData和iv数据解密得到用户数据信息。解密用户数据信息是调用的decryptUser函数,其实现代码如下:
四、解密encryptedData数据得到用户信息:
ini
public ObjectNode decryptUser(String encryptedData, String sessionKey, String iv) {
// 被加密的数据
byte[] dataByte = Base64Utils.decodeFromString(encryptedData);
// 加密秘钥
byte[] keyByte = Base64Utils.decodeFromString(sessionKey);
// 偏移量
byte[] ivByte = Base64Utils.decodeFromString(iv);
try {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length / base + 1;
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
keyByte = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
parameters.init(new IvParameterSpec(ivByte));
cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化
byte[] resultByte = cipher.doFinal(dataByte);
return objectMapper.readValue(new String(resultByte, StandardCharsets.UTF_8), ObjectNode.class);
} catch (Exception e) {
logger.error("用户信息解密失败:{}", e.getMessage());
}
}