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.