微信小程序登录
微信各接口定义(残缺版):
package com.cong.security.core.properties;
public class WXConstant {
/** 获取access_token. */
public static String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
/** 根据openId获取用户信息 */
public static String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";
/** 用oauth2获取用户信息. */
public static String OAUTH2_USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=%s";
/** 验证oauth2的access token是否有效. */
public static String OAUTH2_VALIDATE_TOKEN_URL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s";
/** 微信小程序code换取openId接口 */
public static String CODE_CHANGE_OPENID = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code";
/** 微信小程序消息回复发送接口 */
public static String SEND_MSG = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=";
}
微信小程序登录
返回值封装(code换取openId)
package com.cong.security.core.social.weixin.mini.connect;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class WxminiRes implements Serializable{
private static final long serialVersionUID = 1L;
private String openId;
private String sessionKey;
public WxminiRes(String openId, String sessionKey) {
super();
this.openId = openId;
this.sessionKey = sessionKey;
}
}
微信小程序获取openID
package com.cong.security.core.social.weixin.mini.connect;
import java.security.Security;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import com.cong.security.core.code.CodeException;
import com.cong.security.core.properties.WXConstant;
import org.springframework.web.client.RestTemplate;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class WxMiniOauth2Template {
static {
// BouncyCastle是一个开源的加解密解决方案,主页在http://www.bouncycastle.org/
Security.addProvider(new BouncyCastleProvider());
}
private String appId;
private String appSecret;
public WxMiniOauth2Template(String appId, String appSecret) {
super();
this.appId = appId;
this.appSecret = appSecret;
}
/** * 微信授权码获取openId * * @param code * 授权码 * @return 微信openId * @author single-聪 * @date 2020年7月30日 * @version 1.7.2 */
public WxminiRes getOpenId(String code) {
RestTemplate restTemplate = new RestTemplate();
String url = String.format(WXConstant.CODE_CHANGE_OPENID, this.appId, this.appSecret, code);
String res = restTemplate.getForObject(url, String.class);
log.info("微信小程序登录返回值为:[{}]", res);
JSONObject data = JSONObject.parseObject(res);
String openId = data.getString("openid");
// 返回错误码时直接返回空
if ((data.getString("errcode") == null || !"0".equals(data.getString("errcode")))
&& StringUtils.isNotBlank(openId)) {
// 额外的session_key字段
return new WxminiRes(openId, data.getString("session_key"));
} else {
throw new CodeException("登录失败");
}
}
}
根据前端传输的code换取微信小程序openId,CodeException为自定义异常,目的是为了异常捕获,自己定义一个即可。拿到用户openId即可实现登录。
用户信息解密
主要用于用户不存在时设置默认信息、此时需要用到获取openId是返回的session_key,所以上一步需要合理保存。
返回值封装
package com.cong.security.core.social.weixin.mini.connect;
import java.io.Serializable;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class WeiXinMiniUserInfo implements Serializable{
private static final long serialVersionUID = 1L;
/** 用户唯一标识 */
private String openId;
/** 用户昵称 */
private String nickName;
/** 用户性别 */
private String gender;
/** 普通用户个人资料填写的城市 */
private String city;
/** 普通用户个人资料填写的省份 */
private String province;
/** 国家,如中国为CN */
private String country;
/** 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空 */
private String avatarUrl;
/** 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。 */
private String unionId;
}
解密工具类
package com.cong.util;
import java.security.AlgorithmParameters;
import java.security.Security;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import com.cong.security.core.social.weixin.mini.connect.WeiXinMiniUserInfo;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AesUtil {
static {
// BouncyCastle是一个开源的加解密解决方案,主页在http://www.bouncycastle.org/
Security.addProvider(new BouncyCastleProvider());
}
public static WeiXinMiniUserInfo decrypt(String sessionKey, String encryptedData, String iv) {
// 被加密的数据
byte[] dataByte = Base64.decodeBase64(encryptedData);
// 加密秘钥
byte[] keyByte = Base64.decodeBase64(sessionKey);
// 偏移量
byte[] ivByte = Base64.decodeBase64(iv);
try {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
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");
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);
if (null != resultByte && resultByte.length > 0) {
String result = new String(resultByte, "UTF-8");
log.info("解析出用户数据为:[{}]", result);
WeiXinMiniUserInfo userInfo = JSONObject.parseObject(result, WeiXinMiniUserInfo.class);
return userInfo;
}
} catch (Exception e) {
log.info("微信小程序数据解析异常:[{}]", e.getMessage());
}
return null;
}
}
解密所需参数
获取用户信息
参数 | 解释 | 获取 |
---|---|---|
encryptedData | 包括敏感数据在内的完整用户信息的加密数据 | wx.getUserInfo(Object object) |
iv | 加密算法的初始向量 | wx.getUserInfo(Object object) |
session_key | 会话密钥 | code换取openId时返回 |
注意:可以在上一个接口中返回session_key,但是小程序官网不建议将session_key返回给前端、所以后端需要存储并且在两次接口请求中能够辨别是同一个用户,从而获取到正确的session_key,获取到正确的session_key有有效期、好像只可以使用一次而且只有最新的有效!!!所以前端的login和getUserInfo接口调用有顺序而且有次数规则。(本文为Java后端开发,前端调用简单介绍)
附
因为是第三方登录,所有有两种情况需要考虑:
- 用户已注册:此时小程序用户信息解密失败也是没什么问题,毕竟不会使用这个数据覆盖自己系统中的用户信息
- 用户未注册:此时需要考虑用户信息解密失败的情况,为了良好的用户体验,即使解密失败(可能由于网络等原因解密失败、前一步code2session验证通过实际上这个用户基本上就是真实用户了)也应该设置默认值而不是提示用户注册失败
本文使用的登录是基于Oauth2协议使用JWT生成令牌,不同项目代码嵌入方式不同,所以只封装了和微信那边的对接,至于在自己代码中的业务逻辑自行编写即可。
对接有几天了,可能部分代码会缺失,如有缺失请留言。
还没有评论,来说两句吧...