这页主要是对前端做法的描述,主要描述了前端应该提前通过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;
}
}