前后端 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、模式参数填进去,看在线工具的输出和你代码的输出是否一致,快速定位是哪一方的参数设置有问题。



加载中...