美文网首页
踩坑解密微信小程序登录授权获取手机号

踩坑解密微信小程序登录授权获取手机号

作者: JeffreyTaiT | 来源:发表于2021-04-21 20:06 被阅读0次

    根据 官方文档 微信小程序流程:

    根据官方加密数据解密算法解释:

    1.对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充。

    2.对称解密的目标密文为 Base64_Decode(encryptedData)。

    3.对称解密秘钥 aeskey = Base64_Decode(session_key), aeskey 是16字节。

    4.对称解密算法初始向量 为Base64_Decode(iv),其中iv由数据接口返回。

    总结下前后端一共三步( 如果无需 getUserInfo() )

    1.调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。

    2.调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key

    3.前端调用 getPhoneNumber api 获取 encryptedData 、iv 参数回传后端,后端结合 session_key 进行解密 返回信息


    代码如下:

    1.前端调用login获取code,后端调用code2session获取:

    wx.login({

      success (res) {

        if (res.code) {

          //发起网络请求

          wx.request({

            url: 'https://test.com/onLogin',

            data: {

              code: res.code

            }

          })

        } else {

          console.log('登录失败!' + res.errMsg)

        }

      }

    })

    2.后端接受code参数,进行 code2session 请求获取sessin_key和openId

    //默认的声明

    public static final String ERROR_CODE = "errcode";

        public static final String ERROR_MSG = "errmsg";

        public static final String WX_OPENID = "openid";

        public static final String WX_SESSION_KEY = "session_key";

        public static final String WX_PHONE_NUM = "phoneNumber";

        public static final String WX_PURE_PHONE_NUM = "purePhoneNumber";

        .

        .

        .

    public Map<String, String> getSession(String code) {

            Map<String, String> map = new HashMap<>();

            JSONObject result = null;

            try {

                result = getSessionKeyOrOpenId(code);

            } catch (Exception e) {

                log.error("解密获取sessionKey失败:{}", e);

            }

            log.info("post请求获取的session:{}", result);

            if (StringUtils.isNotBlank(result.getString(ERROR_CODE))) {

                log.error("解密获取sessionKey失败:{}", result.getString(ERROR_MSG));

                throw new BusinessException(result.getString(ERROR_MSG));

            }

            String openId = result.getString(WX_OPENID);

            String sessionKey = result.getString(WX_SESSION_KEY);

            //根据 openid 查询 sessionKey 是否存在

            String existSessionKey = String.valueOf(redisTemplate.opsForValue().get(openId));

            if (StringUtils.isNotBlank(existSessionKey)) {

                //存在 删除 sessionKey 重新生成 sessionKey 返回

                log.info("old session :{}",existSessionKey);

                redisTemplate.delete(openId);

            }

            // 缓存一份新的

            JSONObject sessionObj = new JSONObject();

            sessionObj.put(WX_OPENID, openId);

            sessionObj.put(WX_SESSION_KEY, sessionKey);

            redisTemplate.opsForValue().set(openId, sessionObj.toJSONString());

            //把新的 sessionKey 和 oppenid 返回给小程序

            map.put("sessionKey", sessionKey);

            map.put("openId", openId);

            log.info("get session by code :\nopenid:\n{} \nsession_key:\n{} \ncode:\n{}\n",openId, sessionKey, code);

            return map;

        }

    private JSONObject getSessionKeyOrOpenId(String code) throws Exception {

            //微信端登录code

            String requestUrl = "https://api.weixin.qq.com/sns/jscode2session";

            Map<String, Object> requestUrlParam = new HashMap<String, Object>();

            requestUrlParam.put("appid", appId);

            requestUrlParam.put("secret", appSecret);

            requestUrlParam.put("js_code", code);

            requestUrlParam.put("grant_type", "authorization_code");

            //发送post请求读取调用微信接口获取openid用户唯一标识

            JSONObject jsonObject = JSONObject.parseObject(HttpUtils.sendPostMethod(requestUrl, requestUrlParam, "UTF-8"));

            return jsonObject;

        }

    3.此时前端获取到返回信息,立刻调用 getPhoneNumber api 获取 encryptedData 、iv 参数回传后端,后端根据openId 从redis拿出刚刚的session_key,进行解密

    public WxUserDTO decodeInfo(String openId, String rawData, String signature, String encryptedData, String iv) {

            //根据 openid 获取对应的wx_session

            String existSessionKey = (String) redisTemplate.opsForValue().get(openId);

            if (StringUtils.isBlank(existSessionKey)) {

                throw new BusinessException("无效的openId");

            }

            //获取redis里的wx_session信息

            JSONObject existSession = JSONObject.parseObject(existSessionKey);

            String sessionKey = existSession.getString(WX_SESSION_KEY);

            //填充用户信息

            WxUserDTO wxUserDTO = new WxUserDTO();

            wxUserDTO.setOpenId(openId);

            wxUserDTO.setSessionKey(sessionKey);

            //非敏感用户信息填充

            if (StringUtils.isNoneBlank(rawData)) {

                log.info("用户非敏感信息" + rawData);

                JSONObject rawDataJson = JSON.parseObject(rawData);

                String nickName = rawDataJson.getString("nickName");

                String avatarUrl = rawDataJson.getString("avatarUrl");

                String gender = rawDataJson.getString("gender");

                String city = rawDataJson.getString("city");

                String country = rawDataJson.getString("country");

                String province = rawDataJson.getString("province");

                wxUserDTO.setUbalance(0);

                wxUserDTO.setUaddress(country + " " + province + " " + city);

                wxUserDTO.setUavatar(avatarUrl);

                wxUserDTO.setUgender(Integer.parseInt(gender));

                wxUserDTO.setUname(nickName);

            }

            log.info("decode use by param \nencryptedData:\n{} \nsessionKey:\n{} \niv:\n{}\n",encryptedData, sessionKey, iv);

            JSONObject userInfo = getWxUserInfo(encryptedData, sessionKey, iv);

            String decryptAppid = userInfo.getJSONObject(WATERMARK).getString(APPID);

            if(!appId.equals(decryptAppid)){

                throw new BusinessException("appId 异常,请重试");

            }

            log.info("根据解密算法获取的userInfo=" + userInfo);

            if (null == userInfo || StringUtils.isBlank(userInfo.getString(WX_PHONE_NUM))) {

                throw new BusinessException("获取手机号失败");

            }

            wxUserDTO.setMobile(userInfo.getString(WX_PHONE_NUM));

            if (StringUtils.isBlank(wxUserDTO.getMobile())) {

                wxUserDTO.setMobile(userInfo.getString(WX_PURE_PHONE_NUM));

            }

            UserHelper.savaByOpenid(openId, wxUserDTO);

            loginOrRegisterByWxUser(wxUserDTO);

            return wxUserDTO;

        }

    踩坑记录

    解密核心代码:

    private static JSONObject getWxUserInfo(String encryptedData, String sessionKey, String iv) {

            Base64.Decoder decoder = Base64.getDecoder();

            // 被加密的数据

            byte[] dataByte = decoder.decode(encryptedData);

            // 加密秘钥

            byte[] keyByte = decoder.decode(sessionKey);

            // 偏移量

            byte[] ivByte = decoder.decode(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", "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 JSON.parseObject(result);

                }

            } catch (NoSuchAlgorithmException e) {

                log.error(e.getMessage(), e);

            } catch (NoSuchPaddingException e) {

                log.error(e.getMessage(), e);

            } catch (InvalidParameterSpecException e) {

                log.error(e.getMessage(), e);

            } catch (IllegalBlockSizeException e) {

                log.error(e.getMessage(), e);

            } catch (BadPaddingException e) {

                log.error(e.getMessage(), e);

            } catch (UnsupportedEncodingException e) {

                log.error(e.getMessage(), e);

            } catch (InvalidKeyException e) {

                log.error(e.getMessage(), e);

            } catch (InvalidAlgorithmParameterException e) {

                log.error(e.getMessage(), e);

            } catch (NoSuchProviderException e) {

                log.error(e.getMessage(), e);

            }

            return null;

        }

    注意几个坑:

    先放一个可以解析成功的例子:

    String encryptedData = "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==";

    String sessionKey = "tiihtNczf5v6AKRyjwEUhQ==";

    String iv = "r7BXXKkLb8qrSNn05n0qiA==";

    1.个人开发者账号无法获取到手机号

    2.解密报错 last block incomplete in decryption 报错:

    检查 encryptedData 是否有Decode,是否有%3D %2B等未转译的字符

    3.解密报错 pad block corrupted

    这个错困扰了很久,一般两种情况,一个是前端在回调中,又调用了 wx.login() 的api,导致session_key被冲掉,解密的时候,此时的session_key已经不是加密时的session_key了,自然解密失败。

    另一个就是在点击getPhoneNumber按钮之前session_key已经失效,导致解密失败,建议进入页面即checkSession,如果已过期则刷新。

    核心代码如上,其实放在一步也是可以的。因为编辑器没法格式化,简单记录下,如需详细代码,移步 这里

    相关文章

      网友评论

          本文标题:踩坑解密微信小程序登录授权获取手机号

          本文链接:https://www.haomeiwen.com/subject/oidzlltx.html