官網流程概述
微信官方網頁對於微信小程序獲取用戶手機號的處理描述如下
這頁主要是對前端做法的描述,主要描述了前端應該提前通過wx.login登陸,或者進行登錄態檢查,以此避免刷新登錄態的操作,避免出現服務端存的sessionKey不是最新的sessionKey從而出現敏感數據解密失敗的問題。前端通過button觸發bindgetphonenumber事件,拿到加密數據傳給後端,後端通過解密算法解密。
後端邏輯
微信會對這些開放數據做簽名和加密處理。開發者後臺拿到開放數據後可以對數據進行校驗簽名和解密,來保證數據不被篡改。所以
1、前端通過調用接口(如 wx.getUserInfo)獲取數據時,接口會同時返回 rawData、signature,其中 signature = sha1( rawData + session_key )
2、開發者將 signature、rawData 發送到開發者服務器進行校驗。服務器利用用戶對應的 session_key 使用相同的算法計算出簽名 signature2 ,比對 signature 與 signature2 即可校驗數據的完整性。
數據校驗
後端拿到rawData後通過SHA1()算法對後端存儲的sessionKey進行SHA1(rowData,sessionKey)加密,如果得到的signature與前端傳來的signature一致,則校驗成功。(獲取用戶手機號時數據校驗不是必須的,所以此處只敘述邏輯並未實現)
數據解密
官方文檔對數據解密的解釋是這樣的:
接口如果涉及敏感數據(如wx.getUserInfo當中的 openId 和 unionId),接口的明文內容將不包含這些敏感數據。開發者如需要獲取敏感數據,需要對接口返回的加密數據(encryptedData) 進行對稱解密。 解密算法如下:
對稱解密使用的算法爲 AES-128-CBC,數據採用PKCS#7填充。
對稱解密的目標密文爲 Base64_Decode(encryptedData)。
對稱解密祕鑰 aeskey = Base64_Decode(session_key), aeskey 是16字節。
對稱解密算法初始向量 爲Base64_Decode(iv),其中iv由數據接口返回。
所以筆者進行數據解密時採用了網絡上搜的解密工具使用
我得到簡潔的解密工具如下:
public class AesUtil {
public static String wxDecrypt (String encrypted, String sessionKey, String iv)throws Exception {
byte[] encrypData = Base64.decodeBase64(encrypted);
byte[] ivData = Base64.decodeBase64(iv);
byte[] sKey = Base64.decodeBase64(sessionKey);
String decrypt = decrypt(sKey,ivData,encrypData);
return decrypt;
}
public static String decrypt(byte[] key, byte[] iv, byte[] encData) throws Exception {
AlgorithmParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
//解析解密後的字符串
return new String(cipher.doFinal(encData),"UTF-8");
}
}
但是經測試出現了問題,拋出“BadPaddingException: Given final block not properly padded. Such issues can”開頭的異常
經過問題排查,找出問題
1、微信小程序開發者工具獲取的sessionKey有效期爲5分鐘,過時之後sessionKey就會解不出來
2、完善解密算法,增加處理異常的try–catch塊,更換getInstance參數
解決上述兩個問題之後,優化了工具,拿到用戶手機號併成功返回。
返回格式與微信官方網站上一致。
格式如下
{
"phoneNumber": "13580006666",
"purePhoneNumber": "13580006666",
"countryCode": "86",
"watermark":
{
"appid":"APPID",
"timestamp": TIMESTAMP
}
}
後端實現
此處貼上實現代碼(後端接收由SpringBoot實現):
Controller層實現:
@PostMapping("/member/phone")
@ApiOperation(value = "獲取用戶手機號", notes = "說明:")
public PlatformResult getPhoneNumber(
@ApiParam(name = "encryptedData", value = "加密數據")
@RequestParam("encryptedData") String encryptedData,
@ApiParam(name = "iv", value = "加密算法的初始向量")
@RequestParam("iv") String iv) throws Exception {
//TODO:從redis裏獲取sessionKey
String sessionKey = (String) redisUtil.get(sessionKeyPre + jwtTokenInfoUtil.getMemberIdByToken());
if (null != sessionKey && !sessionKey.isEmpty()) {
String s = AesUtil.wxDecrypt(encryptedData, sessionKey, iv);
JSONObject object = JSONObject.parseObject(s);
Object number = object.get("phoneNumber");
if (null != number) {
MeMemberInfo memberInfo = new MeMemberInfo();
memberInfo.setId(jwtTokenInfoUtil.getMemberIdByToken());
memberInfo.setPhone(number.toString());//更新綁定手機號
memberInfo.setPhoneStatus(ComConstants.PHONE_STATUS_0);//置爲綁定手機狀態
readMeMemberInfoService.updateById(memberInfo);
return PlatformResult.success(number.toString());
}
}
return PlatformResult.failure(ResultCode.RESULE_DATA_NONE);
}
解密工具AesUtil實現:
package com.andrea.platform.util;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
/**
* @program: aes-service
* @description: 加密信息解密
* @author: Andrea_nul
* @create: 2019-12-04 10:57
**/
public class AesUtil {
public static String wxDecrypt (String encrypted, String sessionKey, String iv)throws Exception {
byte[] encrypData = Base64.decodeBase64(encrypted);
byte[] ivData = Base64.decodeBase64(iv);
byte[] sKey = Base64.decodeBase64(sessionKey);
String decrypt = decrypt(sKey,ivData,encrypData);
return decrypt;
}
public static String decrypt(byte[] key, byte[] iv, byte[] encData) throws Exception {
// AlgorithmParameterSpec ivSpec = new IvParameterSpec(iv);
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
// cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
//解析解密後的字符串
String resultString = null;
AlgorithmParameterSpec ivSpec = new IvParameterSpec(iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
resultString = new String(cipher.doFinal(encData), "UTF-8");
} catch (Exception e) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
resultString = new String(cipher.doFinal(encData), "UTF-8");
}
return resultString;
}
}