前後端 AES 對接,十次有八次會出問題
不是在誇張。AES 加解密的參數太多了——模式、填充、密鑰長度、IV、編碼格式——任何一個對不上,解出來的要麼是亂碼,要麼直接報錯。更麻煩的是,兩邊各自跑起來都是正常的,一聯調就壞。
這篇文章把對接 AES 最容易出問題的地方挨個過一遍,遇到問題對照着查。
第一關:密鑰長度決定用的是哪種 AES
AES 有三種規格:AES-128、AES-192、AES-256,對應密鑰長度分別是 16 字節、24 字節、32 字節。
這裏有個坑——很多人直接把一個字符串當密鑰傳進去,比如 "mysecretkey",這才11個字符,不夠16字節。不同的庫對這種情況處理方式不一樣:有的會自動補零,有的會截斷,有的直接報錯。
# Python 裏用 AES,密鑰必須是 16/24/32 字節
from Crypto.Cipher import AES
key = b"mysecretkey12345" # 正好16字節,AES-128
key = b"mysecretkey123456789abcd" # 24字節,AES-192
key = b"mysecretkey123456789abcdefghijk" # 32字節,AES-256
如果前端用的是字符串密鑰,後端收到後要用同樣的處理方式——要麼都截斷到固定長度,要麼都用 MD5/SHA256 把密鑰哈希成固定長度,但兩邊必須一致。
第二關:IV 到底怎麼傳?
用 CBC 模式必須有 IV(初始化向量),IV 是16字節。
最常見的錯誤有兩種:
一是 IV 沒傳,用了默認值,但兩邊默認值不一樣。 有些庫默認 IV 全是零字節,有些庫不允許省略 IV。兩邊各自測試都正常,聯調時一方用了全零 IV,另一方用了隨機 IV,解出來必然是亂碼。
二是 IV 的傳輸方式沒約定好。 標準做法是把隨機生成的 IV 拼在密文前面一起傳,解密時先取前16字節當 IV,剩下的纔是密文。但有的系統把 IV 單獨放在 Header 裏傳,有的用固定 IV——這些都行,但兩邊必須說好。
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
# 加密:隨機生成 IV,拼在密文前面
key = b"mysecretkey12345"
iv = os.urandom(16) # 隨機16字節 IV
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(b"hello world", AES.block_size))
result = base64.b64encode(iv + ciphertext).decode() # IV + 密文一起 Base64
# 解密:先取前16字節當 IV
raw = base64.b64decode(result)
iv = raw[:16]
ciphertext = raw[16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
第三關:填充方式(Padding)
AES 是分組密碼,每次處理16字節。明文長度不是16的倍數時,需要填充。
最常用的是 PKCS7(也叫 PKCS5,兩者在 AES 場景下完全一樣)。填充規則:缺幾個字節就用那個數字填,比如缺3字節就填三個 \x03。
問題來了:如果解密時用了錯誤的填充方式,或者加密時用了零填充(ZeroPadding)但解密時用了 PKCS7,解出來要麼亂碼要麼報錯。
# CryptoJS(前端)默認用 Pkcs7
var encrypted = CryptoJS.AES.encrypt(message, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7 // 默認就是這個,可以不寫
});
# Python pycryptodome 也是 PKCS7
from Crypto.Util.Padding import pad
padded = pad(plaintext.encode(), AES.block_size) # 默認 pkcs7
如果後端是 Java,要注意 Cipher.getInstance("AES/CBC/PKCS5Padding") 裏寫的是 PKCS5,實際上用的也是 PKCS7 的算法,是同一個東西,不用擔心。
第四關:Base64 還是 Hex?
加密後的結果是二進制字節,不能直接當字符串傳。通常有兩種編碼方式:
- Base64:更短,常見於 Web API
- Hex:可讀性好,常見於調試和部分後端系統
兩邊必須用同一種。前端 CryptoJS 加密後 .toString() 默認輸出 Base64,如果後端用 Hex 去解碼,肯定出錯。
另外,Base64 有兩個變體:標準 Base64 和 URL-safe Base64。標準 Base64 用 + 和 /,URL-safe 用 - 和 _。如果密文要放在 URL 參數裏傳,必須用 URL-safe,否則 + 會被當成空格解析。
import base64
# 標準 Base64
encoded = base64.b64encode(ciphertext).decode()
decoded = base64.b64decode(encoded)
# URL-safe Base64
encoded = base64.urlsafe_b64encode(ciphertext).decode()
decoded = base64.urlsafe_b64decode(encoded)
第五關:字符編碼(UTF-8 還是 GBK?)
加密的是字符串,字符串要先轉成字節才能加密。轉換時用什麼編碼,解密後轉回字符串時就必須用什麼編碼。
前端 JavaScript 默認是 UTF-8,但如果後端是老系統,可能用 GBK。一旦不一致,解出來的中文就是亂碼。
// 前端 CryptoJS,輸入字符串時內部用 UTF-8 處理
var message = CryptoJS.enc.Utf8.parse("你好世界");
# 後端 Python,解密後要用 utf-8 解碼
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size).decode("utf-8")
# 如果是 GBK 編碼的系統
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size).decode("gbk")
排查思路
遇到 AES 對接出問題,按這個順序排查:
- 先隔離問題:用同一種語言在本地加密再解密,確認單邊沒問題
- 固定所有參數:模式、填充、密鑰、IV 全部約定死,先用固定 IV(比如全零)排除 IV 傳輸問題
- 打印中間結果:把加密後的 Base64 或 Hex 值打出來,兩邊對比,確認密文是一樣的
- 逐項排查:密鑰 → IV → 模式 → 填充 → 編碼,一個一個對
調試過程中可以用 AES 在線加解密工具 作爲基準,把你的密鑰、IV、模式參數填進去,看在線工具的輸出和你代碼的輸出是否一致,快速定位是哪一方的參數設置有問題。



加載中...