Step on the pit to decrypt wechat applet login authorization to obtain mobile phone number

Decrypt wechat applet login authorization to obtain mobile phone number

according to Official documents Wechat applet process:

According to the interpretation of the official encryption data decryption algorithm:

1. The algorithm used for symmetric decryption is AES-128-CBC, and the data is PKCS#7 filled.
2. The target ciphertext of symmetric decryption is Base64_Decode(encryptedData).
3. Symmetric decryption key aeskey = Base64_Decode(session_key), aeskey is 16 bytes.
4. The initial vector of symmetric decryption algorithm is Base64_Decode(iv), where iv is returned by the data interface.

Summarize the following three steps (if getUserInfo() is not required) in the front and back end
1. Call Wx Login() obtains the temporary login certificate code and returns it to the developer server.

2. Call auth Code2session interface in exchange for the user's unique ID OpenID, the user's unique ID UnionID under the wechat open platform account (if the current applet has been bound to the wechat open platform account) and the session key session_key.

3. The front end calls the getPhoneNumber api to obtain the encryptedData and iv parameters and returns them to the back end. The back end combines the session_key decrypts the returned information

The code is as follows:

1. The front end calls login to get the code, and the back end calls code2session to get:

wx.login({
  success (res) {
    if (res.code) {
      //Initiate network request
      wx.request({
        url: 'https://test.com/onLogin',
        data: {
          code: res.code
        }
      })
    } else {
      console.log('Login failed!' + res.errMsg)
    }
  }
})

2. The back-end accepts the code parameter and makes a code2session request to obtain sessin_key and openId

	//Default declaration
	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("Decryption acquisition sessionKey fail:{}", e);
        }

        log.info("post Requested session:{}", result);
        if (StringUtils.isNotBlank(result.getString(ERROR_CODE))) {
            log.error("Decryption acquisition sessionKey fail:{}", result.getString(ERROR_MSG));
            throw new BusinessException(result.getString(ERROR_MSG));
        }

        String openId = result.getString(WX_OPENID);
        String sessionKey = result.getString(WX_SESSION_KEY);

        //Query whether sessionKey exists according to openid
        String existSessionKey = String.valueOf(redisTemplate.opsForValue().get(openId));
        if (StringUtils.isNotBlank(existSessionKey)) {
            //Exists. Delete sessionKey and regenerate sessionKey. Return
            log.info("old session :{}",existSessionKey);
            redisTemplate.delete(openId);
        }
        // Cache a new
        JSONObject sessionObj = new JSONObject();
        sessionObj.put(WX_OPENID, openId);
        sessionObj.put(WX_SESSION_KEY, sessionKey);
        redisTemplate.opsForValue().set(openId, sessionObj.toJSONString());

        //Return the new sessionKey and oppenid to the applet
        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 {
        //Wechat login 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");

        //Send a post request to read and call the wechat interface to obtain the unique ID of the openid user
        JSONObject jsonObject = JSONObject.parseObject(HttpUtils.sendPostMethod(requestUrl, requestUrlParam, "UTF-8"));
        return jsonObject;
    }

3. At this time, the front end obtains the return information, immediately calls getPhoneNumber api to obtain encryptedData and iv parameters, and returns them to the back end. The back end takes out the just session from redis according to openId_ Key, decrypt

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

        //Obtain the corresponding Wx according to openid_ session
        String existSessionKey = (String) redisTemplate.opsForValue().get(openId);
        if (StringUtils.isBlank(existSessionKey)) {
            throw new BusinessException("invalid openId");
        }
        //Get Wx in redis_ Session information
        JSONObject existSession = JSONObject.parseObject(existSessionKey);
        String sessionKey = existSession.getString(WX_SESSION_KEY);

        //Fill in user information
        WxUserDTO wxUserDTO = new WxUserDTO();
        wxUserDTO.setOpenId(openId);
        wxUserDTO.setSessionKey(sessionKey);

        //Non sensitive user information filling
        if (StringUtils.isNoneBlank(rawData)) {
            log.info("User non sensitive information" + 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 Exception, please try again");
        }

        log.info("Obtained according to the decryption algorithm userInfo=" + userInfo);
        if (null == userInfo || StringUtils.isBlank(userInfo.getString(WX_PHONE_NUM))) {
            throw new BusinessException("Failed to obtain mobile phone number");
        }

        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;
    }

Decrypt core code:

private static JSONObject getWxUserInfo(String encryptedData, String sessionKey, String iv) {
        Base64.Decoder decoder = Base64.getDecoder();
        // Encrypted data
        byte[] dataByte = decoder.decode(encryptedData);
        // Encryption key
        byte[] keyByte = decoder.decode(sessionKey);
        // Offset
        byte[] ivByte = decoder.decode(iv);
        try {
            // If the key is less than 16 bits, supplement it The content in this if is very important
            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;
            }
            // initialization
            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));
            // initialization
            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;
    }

Note several pits:

Let's start with an example of successful parsing:

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

1. The mobile phone number cannot be obtained from the individual developer account

2. last block incomplete in decryption error:
Check whether the encryptedData has Decode and whether there are untranslated characters such as% 3D and% 2B

3. Decryption error pad block corrupted
This error has been bothered for a long time. There are two general situations: one is that the front end calls Wx again in the callback Login() api, resulting in session_ When the key is flushed and decrypted, the session_ The key is no longer the session during encryption_ The key failed. Naturally, the decryption failed.
The other is the session before clicking the get phonenumber button_ The key has expired, resulting in decryption failure. It is recommended to enter the page, i.e. checkSession. If it has expired, refresh it.

Correct any mistakes.

Keywords: Java Back-end Mini Program Encryption openid

Added by galmar on Thu, 03 Mar 2022 14:19:19 +0200