一、问题背景及原因分析
需求对保密性要求严格点,就用的 AES + 盐值 + 偏移向量 去做,前端加密传递参数,Java 解密参数,然后查询数据,得到数据后再将数据加密返给前端,前端最对数据进行解密,得到具体数据使用。
在此过程中发现偶尔使用 Java AES 解密前端传递的参数时会报这个异常,如下:
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:922)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:833)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at javax.crypto.Cipher.doFinal(Cipher.java:2165)
at com.symmetric.aes.TestAES.testDecrpyt(TestAES.java:200)
at com.symmetric.aes.TestAES.main(TestAES.java:48)
字面理解很容易,就是解密的字符串的数组必须是 16 的倍数。
这里有一篇文章介绍:https://blog.csdn.net/kzcming/article/details/80019478,可以看一下。
1、分析出现此异常的情况:
如果不把加密后的数组拼接为字符串,直接返回,然后使用这个加密后的数组进行解密就没有任何错误;
但是把加密后的数组拼接为字符串,然后解密时在把此字符串转为数组,就会出现此异常
2、具体分析:
发现当把字节数组转为字符串后,在把 字符串.getBytes() 获得字节数组,发现两个字节数组前后不一样了 —— 这是报错的位置所在。(声明:new String(byte[]) 和 "str".getBytes() 两个方法使用的编码一样,然后换成其他编码也出现这样情况,也就是说不是编码的问题)
3、原因
(1)为什么数组转字符串,字符串然后转数组会出现,前后两个字节数组的值会不同?
因为并不是每个字节数和编码集上的字符都有对应关系,如果一个字节数在编码集上没有对应,编码 new String(byte[]) 后往往解出来的会是一些乱码无意义的符号,例如:��。
但是解码的时候 � 这个字符也是一个字符在编码表中也有固定的字节数用来表示,所有解码出来的值必定是编码表中对应的值,除非你的字节数组中的字节数正好在编码表中有对应的值,否则编码、解码后的字节数组会不一样。
误区:误以为所有的字节数组都可以new String(),然后在通过String.getBytes()还原。
(2)再说这个异常报错:解密的字节数组必须是16的倍数,这得从AES的原理说起,AES是把数据按16字节分组加密的,所有如果数组长度不是16的倍数会报错。
AES原理:AES是对数据按128位,也就是16个字节进行分组进行加密的,每次对一组数据加密需要运行多轮,而输入密钥的长度可以为128、192和256位,也就是16个字节、24个字节和32个字节,如果用户输入的密钥长度不是这几种长度,也会补成这几种长度。
无论输入密钥是多少字节,加密还是以16字节的数据一组来进行的,密钥长度的不同仅仅影响加密运行的轮数。
4、解决的办法:
(1)可以用 base64 对产生的数组进行编码,然后在解码,这样不会像 new String(byte[])、getBytes() 那样造成数组前后不一致,一开始我看到大部分人都是用 base64,我也只是以为多一层编码看起来安全一些而已,没想到 base64 对数组的处理是不会造成误差的
(2)就是直接返回数组,然后再用数组解密咯
二、解决方案
而我本身就是采用了base64 编码的,结果还是偶尔出现这个报错,后来发现了规律,就是只有前端加密的字符串包含特殊字符,如 + ,传递给后台去解密就一定会报这个错。而我本身就进行了 encodeURIComponent() 进行传参。
后来了解到原来原因在这里:由于前台通过 url 传过来的加密后的数据到后台接受丢失特殊字符(url 对字符串进行编码,但是发现+全部都变成了空格),然后断点调试一下,确实 + 变成了 空格
1、解决方式一:Get 参数需要对 URL特殊字符进行转义
// 对前台的代码进行编码
bankCardNumber = bankCardNumber.replace(/\+/g,"%2B");
// 后台再转码回去 - 就是替换
encrypted = encrypted.replaceAll("%2B", "\\+");
(1)知识
1、URL特殊字符需转义
2、空格换成加号(+)
3、正斜杠(/)分隔目录和子目录
4、问号(?)分隔URL和查询
5、百分号(%)制定特殊字符
6、#号指定书签
7、&号分隔参数
(2)转义字符的原因:
如果你的表单使用get方法提交,并且提交的参数中有“&”等特殊符的话,如果不做处理,在service端就会将&后面的作为另外一个参数来看待。例如表单的action为list.jsf?act=Go&state=5,则提交时通过request.getParameter可以分别取得act和state的值。 如果你的本意是act='go&state=5'这个字符串,那么为了在服务端拿到act的准确值,你就必须对&进行转义。
(3)url 转义字符原理:
将这些特殊的字符转换成ASCII码,格式为:%加字符的ASCII码,即一个百分号%,后面跟对应字符的ASCII(16进制)码值。例如 空格的编码值是"%20"。
(4)URL特殊符号及对应的十六进制值编码:
1、+ URL 中+号表示空格 %2B
2、空格 URL中的空格可以用+号或者编码 %20
3、/ 分隔目录和子目录 %2F
4、? 分隔实际的 URL 和参数 %3F
5、% 指定特殊字符 %25
6、# 表示书签 %23
7、& URL 中指定的参数间的分隔符 %26
8、= URL 中指定参数的值 %3D
2、解决方式二:使用 post 在 body 里传参即可(这样就不会存在需 URL 转义的问题)
@PostMapping("/downUrl")
public OperationInfo getDownUrl (@RequestBody String jsonStr) throws Exception{
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
String idStr = jsonObject.getString("idStr");
......
}
这里就涉及到 Java 对象与 JSON 对象之间的相互转换。