微信小程序的授权登录-Java 后端 (Spring boot)

 

微信开发文档链接: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是否每次登入都需要刷新?这么些个问题,自己结合业务需求来做判断,我这里为了简单起见,直接放数据库里了
 


来源:春哥技术博客,欢迎分享,转载请注明出处。(欢迎加春哥团队客服微信号:taike668)

本文地址:https://www.cgtblog.com/cgymlt/10597.html
上一篇:uniapp开发小程序uview的u-button样式      下一篇:微信小程序 之 原生开发