微信小程序获取用户绑定的手机号信息解密
最近做微信小程序获取用户绑定的手机号信息解密,试了很多方法。最终虽然没有完全解决,但是也达到我的极限了。有时会报错: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==
1
下面是解密后的信息
{ "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依赖。
<!--微信小程序进行解密加密的用户信息 --> <dependency> <groupId>org.codehaus.xfire</groupId> <artifactId>xfire-core</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk16</artifactId> <version>1.46</version> </dependency>
解密代码
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加密的数据,当然会报错。