最近做微信小程序获取用户绑定的手机号信息解密,试了很多方法。最终虽然没有完全解决,但是也达到我的极限了。有时会报错:javax.crypto.BadPaddingException: pad block corrupted。
每次刚进入小程序登陆获取手机号时,会出现第一次解密失败,再试一次就成功的问题。如果连续登出,登入,就不会再出现揭秘失败的问题。但是如果停止操作过一会,登出后登入,又会出现第一次揭秘失败,再试一次就成功的问题。
网上说的,官方文档上注意点我都排除了。获取的加密密文是在前端调取wx.login()方法后,调用我后端的微信授权接口,获取用户的sessionkey,openId.然后才是前端调用的获取sessionkey加密的用户手机号接口,所以我可以保证每次sessionkey是最新的。不会过期。
并且我通过日志发现在sessionkey不变的情况下,第一次失败,第二次解密成功。
微信为了安全,把解密的key,和加密的用户数据分成了两步,分别给了前台,后台。这样,如果不监听到两次请求,是无法解密的。具体步骤:
1: 前端调取微信获取code接口
2: 在通过code调用后台授权登陆接口,后台通过code换取用户的openid,sessionKey,unionid.并将这写信息保存到redis
3.前端通过button,经用户同意后获取到加密的用户信息,调用后台接口进行解密。
encryptedData:f7KBxq7XZ1SGBYAVNUPLqsBX6bdKLTTwlN+BvvKg1YY92eOxg0EisRRRlYYG2ZAbpDlCeWT1GoGYvk4nrc0brLPrwgIfSSDQo7QAq5iUFtXi7t/p3p/t8CrS28rJB9niTsrteS8g78tASaiDB1WIt34Qjx0TjtIIMjFVY/FXjRtltuve3ADPrefYOkBoU2TRCpXXvdBIxTFx6GyyzZb/pQ==,iv:PD89jqhSTSDCB3dA146Ffw==,sessionKey:xAunW5fXE1dEdMZYsx1AbA==
下面是解密后的信息
{
"phoneNumber":"182****6271",
"purePhoneNumber":"182****271",
"countryCode":"86",
"watermark":{
"timestamp":1566963882,
"appid":"wx7b****c3474687"
}
}
4.后台通过解密算法。获取如上出参,并根据需求返回手机号等。
如果调用接口第一次出现了异常:javax.crypto.BadPaddingException: pad block corrupted
,则返回页面一个可以判断的code,这样前端就可以提示用户再试一次,一般不会出现两次都报错的情况,两次至少一次能够成功。
如果不能接受上述方案,还有一种方案就是让前端在获取用户手机号这个button按钮中,先调用后台的微信授权接口两次,你没看错,调用两次。再去调用获取用户手机号,这是获取的密文是可以一遍过的。经验是这样的。具体原因不清楚。
解密的代码是我在网络的大海里掏出来了,真是不容易。亲测,并在使用。下面贴出来。再补上一嘴:下面的并没有pom的专门引入。如果实在导不进来,在用下面给的pom依赖。
org.codehaus.xfire
xfire-core
1.2.6
org.bouncycastle
bcprov-jdk16
1.46
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.encoders.Base64;
import com.alibaba.fastjson.JSONObject;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.InvalidParameterSpecException;
private String decryptNew(String encryptedData, String sessionKey, String iv) throws Exception {
String result = "";
// 被加密的数据
byte[] dataByte = Base64.decode(encryptedData);
// 加密秘钥
byte[] keyByte = Base64.decode(sessionKey);
// 偏移量
byte[] ivByte = Base64.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) {
result = new String(resultByte, "UTF-8");
}
} catch (NoSuchAlgorithmException e) {
LOGGER.error(e.getMessage(), e);
} catch (NoSuchPaddingException e) {
LOGGER.error(e.getMessage(), e);
} catch (InvalidParameterSpecException e) {
LOGGER.error(e.getMessage(), e);
} catch (IllegalBlockSizeException e) {
LOGGER.error(e.getMessage(), e);
} catch (BadPaddingException e) {
LOGGER.error(e.getMessage(), e);
} catch (UnsupportedEncodingException e) {
LOGGER.error(e.getMessage(), e);
} catch (InvalidKeyException e) {
LOGGER.error(e.getMessage(), e);
} catch (InvalidAlgorithmParameterException e) {
LOGGER.error(e.getMessage(), e);
} catch (NoSuchProviderException e) {
LOGGER.error(e.getMessage(), e);
}
return result;
}
现在找到问题的解决方案。这个锅还真不是后端的问题。上面的工具类也是没问题的。出现偶尔报错的直接原因是前端同学调用的流程不太明白,导致在获取用户手机号的回调函数中,又调用了一遍login()方法,然后把加密数据传给我们来解密!
在回调中不应该调用登陆的方法,这样会导致刷新sessionkey,当我们用之前保存的sessionkey时,有小概率事件,甚至很大可能导致sessionkey过期。用旧的sessionkey解密显得sessionkey加密的数据,当然会报错。