一、問題背景及原因分析
需求對保密性要求嚴格點,就用的 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 對象之間的相互轉換。