相关推荐recommended
微信小程序的授权登录-Java 后端 (Spring boot)
作者:mmseoamin日期:2024-02-02

微信开发文档链接:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

1. 前提

  • 一个可以测试的微信小程序
  • 此微信小程序的APPID和APPscret(至开发者后台获取)

    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
      @Setter
      public 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
      @Setter
      public class RawDataDO {
          private String nickName;
          private String avatarUrl;
          private Integer gender;
          private String city;
          private String country;
          private String province;
      }

      用户DO

      @Getter
      @Setter
      public 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 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 paramList = new ArrayList<>();
                      for (Entry 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 getUserInfoMap(WechatLoginRequest loginRequest) throws Exception;
      }

      Service impl

      @Service
      public 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 getUserInfoMap(WechatLoginRequest loginRequest) throws Exception {
              Map 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 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 login(
                  @Validated @RequestBody WechatLoginRequest loginRequest) throws Exception {
       
              Map userInfoMap = wechatService.getUserInfoMap(loginRequest);
              return userInfoMap;
          }
      }

      写在最后

      一些注意事项:

      • code是有时效行的,5分钟内有效,并且只能使用一次
      • token的实现,以及token过期时间,token放在数据库中还是缓存中,token是否每次登入都需要刷新?这么些个问题,自己结合业务需求来做判断,我这里为了简单起见,直接放数据库里了