微信开发文档链接:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
1. 前提
2. 开发流程
从时序图我们可以了解到流程大致分为两步:
- 小程序端获取code后传给Java后台
- Java后台获取code后向微信后台接口获取open_id
2.1 小程序端(前端要做的)
在微信小程序的前端调用wx.login()获取一个code,这个code就像是我们去微信后台服务器获取用户信息的一个钥匙,微信通过获取这个code的过程给用户一个选择是否授权的选择,如果用户选择了授权就会返回一个code。这个code是一次性的,也是有时限的。
这里简单的做一个说明,首先由小程序端调用wx.login()去获取code,然后,再通过wx.getUserInfo()去获取用户信息(这里请求login和getUserInfo是一起的,把这两次请求的数据合并发给服务端的login接口),通过请求,把:
1.code //临时登入凭证// 如果不同意获取用户信息,则下面四个参数获取不到2.rawData //用户非敏感信息,头像和昵称之类的3.signature //签名4.encryteDate //用户敏感信息,需要解密,(包含unionID)5.iv //解密算法的向量
给到服务端,服务端根据 appid+secret+js_code+grant_type
去请求,获取到session_key和openid(这里无法获取unionID),通过session_key,iv来解密encrypteDate获取用户敏感信息和unionID,把用户信息保存到数据库。然后,我们把sesssoin_key和openid保存下来,与token(自定义登入状态)来进行关联,最后把小程序需要的数据返回给小程序端,以后就通过token来维护用户登入状态。
用户表结构设计:
CREATE TABLE `wechat_user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `token` varchar(100) NOT NULL COMMENT 'token', `nickname` varchar(100) DEFAULT NULL COMMENT '用户昵称', `avatar_url` varchar(500) DEFAULT NULL COMMENT '用户头像', `gender` int(11) DEFAULT NULL COMMENT '性别 0-未知、1-男性、2-女性', `country` varchar(100) DEFAULT NULL COMMENT '所在国家', `province` varchar(100) DEFAULT NULL COMMENT '省份', `city` varchar(100) DEFAULT NULL COMMENT '城市', `mobile` varchar(100) DEFAULT NULL COMMENT '手机号码', `open_id` varchar(100) NOT NULL COMMENT '小程序openId', `union_id` varchar(100) DEFAULT '' COMMENT '小程序unionId', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted_at` timestamp NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`), KEY `idx_open_id` (`open_id`), KEY `idx_union_id` (`union_id`), KEY `idx_mobile` (`mobile`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='小程序用户表';
具体代码
说明,如果@Getter报错,那就删掉,自己加Getter,Setter,@Api开头的注解是swagger的注解,不需要的可以删掉
请求类
@ApiModel@Getter@Setterpublic class WechatLoginRequest { @NotNull(message = "code不能为空") @ApiModelProperty(value = "微信code", required = true) private String code; @ApiModelProperty(value = "用户非敏感字段") private String rawData; @ApiModelProperty(value = "签名") private String signature; @ApiModelProperty(value = "用户敏感字段") private String encryptedData; @ApiModelProperty(value = "解密向量") private String iv;}
非敏感信息DO
@Getter@Setterpublic class RawDataDO { private String nickName; private String avatarUrl; private Integer gender; private String city; private String country; private String province;}
用户DO
@Getter@Setterpublic class WechatUserDO { private Integer id; private String token; private String nickname; private String avatarUrl; private Integer gender; private String country; private String province; private String city; private String mobile; private String openId; private String unionId; private String createdAt; private String updatedAt;}
HttpClientUtils
public class HttpClientUtils { final static int TIMEOUT = 1000; final static int TIMEOUT_MSEC = 5 * 1000; public static String doPost(String url, Map<String, String> paramMap) throws IOException { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建参数列表 if (paramMap != null) { List<NameValuePair> paramList = new ArrayList<>(); for (Entry<String, String> param : paramMap.entrySet()) { paramList.add(new BasicNameValuePair(param.getKey(), param.getValue())); } // 模拟表单 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList); httpPost.setEntity(entity); } httpPost.setConfig(builderRequestConfig()); // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (Exception e) { throw e; } finally { try { response.close(); } catch (IOException e) { throw e; } } return resultString; } private static RequestConfig builderRequestConfig() { return RequestConfig.custom() .setConnectTimeout(TIMEOUT_MSEC) .setConnectionRequestTimeout(TIMEOUT_MSEC) .setSocketTimeout(TIMEOUT_MSEC).build(); }}
service
public interface WechatService { Map<String, Object> getUserInfoMap(WechatLoginRequest loginRequest) throws Exception;}
Service impl
@Servicepublic class WechatServiceImpl implements WechatService { private static final String REQUEST_URL = "https://api.weixin.qq.com/sns/jscode2session"; private static final String = "authorization_code"; @Override public Map<String, Object> getUserInfoMap(WechatLoginRequest loginRequest) throws Exception { Map<String, Object> userInfoMap = new HashMap<>(); // logger报错的话,删掉就好,或者替换为自己的日志对象 logger.info("Start get SessionKey,loginRequest的数据为:" + JSONObject.toJSONString(loginRequest)); JSONObject sessionKeyOpenId = getSessionKeyOrOpenId(loginRequest.getCode()); // 这里的ErrorCodeEnum是自定义错误字段,可以删除,用自己的方式处理 Assert.isTrue(sessionKeyOpenId != null, ErrorCodeEnum.P01.getCode()); // 获取openId && sessionKey String openId = sessionKeyOpenId.getString("openid"); // 这里的ErrorCodeEnum是自定义错误字段,可以删除,用自己的方式处理 Assert.isTrue(openId != null, ErrorCodeEnum.P01.getCode()); String sessionKey = sessionKeyOpenId.getString("session_key"); WechatUserDO insertOrUpdateDO = buildWechatUserDO(loginRequest, sessionKey, openId); // 根据code保存openId和sessionKey JSONObject sessionObj = new JSONObject(); sessionObj.put("openId", openId); sessionObj.put("sessionKey", sessionKey); // 这里的set方法,自行导入自己项目的Redis,key自行替换,这里10表示10天 stringJedisClientTem.set(WechatRedisPrefixConstant.USER_OPPEN_ID_AND_SESSION_KEY_PREFIX + loginRequest.getCode(), sessionObj.toJSONString(), 10, TimeUnit.DAYS); // 根据openid查询用户,这里的查询service自己写,就不贴出来了 WechatUserDO user = wechatUserService.getByOpenId(openId); if (user == null) { // 用户不存在,insert用户,这里加了个分布式锁,防止insert重复用户,看自己的业务,决定要不要这段代码 if (setLock(WechatRedisPrefixConstant.INSERT_USER_DISTRIBUTED_LOCK_PREFIX + openId, "1", 10)) { // 用户入库,service自己写 insertOrUpdateDO.setToken(getToken()) wechatUserService.save(insertOrUpdateDO); userInfoMap.put("token", insertOrUpdateDO.getToken()) } } else { userInfoMap.put("token", wechatUser.getToken()); // 已存在,做已存在的处理,如更新用户的头像,昵称等,根据openID更新,这里代码自己写 wechatUserService.updateByOpenId(insertOrUpdateDO); } return userInfoMap; } // 这里的JSONObject是阿里的fastjson,自行maven导入 private JSONObject getSessionKeyOrOpenId(String code) throws Exception { Map<String, String> requestUrlParam = new HashMap<>(); // 小程序appId,自己补充 requestUrlParam.put("appid", APPID); // 小程序secret,自己补充 requestUrlParam.put("secret", SECRET); // 小程序端返回的code requestUrlParam.put("js_code", code); // 默认参数 requestUrlParam.put("grant_type", GRANT_TYPE); // 发送post请求读取调用微信接口获取openid用户唯一标识 String result = HttpClientUtils.doPost(REQUEST_URL, requestUrlParam); return JSON.parseObject(result); } private WechatUserDO buildWechatUserAuthInfoDO(WechatLoginRequest loginRequest, String sessionKey, String openId){ WechatUserDO wechatUserDO = new WechatUserDO(); wechatUserDO.setOpenId(openId); if (loginRequest.getRawData() != null) { RawDataDO rawDataDO = JSON.parseObject(loginRequest.getRawData(), RawDataDO.class); wechatUserDO.setNickname(rawDataDO.getNickName()); wechatUserDO.setAvatarUrl(rawDataDO.getAvatarUrl()); wechatUserDO.setGender(rawDataDO.getGender()); wechatUserDO.setCity(rawDataDO.getCity()); wechatUserDO.setCountry(rawDataDO.getCountry()); wechatUserDO.setProvince(rawDataDO.getProvince()); } // 解密加密信息,获取unionID if (loginRequest.getEncryptedData() != null){ JSONObject encryptedData = getEncryptedData(loginRequest.getEncryptedData(), sessionKey, loginRequest.getIv()); if (encryptedData != null){ String unionId = encryptedData.getString("unionId"); String phone = encryptedData.getString("phoneNumber"); wechatUserDO.setUnionId(unionId); } } return wechatUserDO; } private JSONObject getEncryptedData(String encryptedData, String sessionkey, String iv) { // 被加密的数据 byte[] dataByte = Base64.decode(encryptedData); // 加密秘钥 byte[] keyByte = Base64.decode(sessionkey); // 偏移量 byte[] ivByte = Base64.decode(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); if (null != resultByte && resultByte.length > 0) { String result = new String(resultByte, "UTF-8"); return JSONObject.parseObject(result); } } catch (Exception e) { logger.error("解密加密信息报错", e.getMessage()); } return null; } private boolean setLock(String key, String value, long expire) throws Exception { boolean result = stringJedisClientTem.setNx(key, value, expire, TimeUnit.SECONDS); return result; } private String getToken() throws Exception { // 这里自定义token生成策略,可以用UUID+sale进行MD5 return ""; }}
Controller
@RestController("LoginController")@RequestMapping(value = "/wechat/login")public class LoginController { @Resource WechatService wechatService; @ApiOperation(value = "1.登入接口", httpMethod = "POST") @PostMapping("/save") public Map<String, Object> login( @Validated @RequestBody WechatLoginRequest loginRequest) throws Exception { Map<String, Object> userInfoMap = wechatService.getUserInfoMap(loginRequest); return userInfoMap; }}
写在最后
一些注意事项:
- code是有时效行的,5分钟内有效,并且只能使用一次
- token的实现,以及token过期时间,token放在数据库中还是缓存中,token是否每次登入都需要刷新?这么些个问题,自己结合业务需求来做判断,我这里为了简单起见,直接放数据库里了