为什么js的AES的加密每次得到的结果都不一样,并且无法在后端被java正常解密呢?

先说需求,需要前端对字符串 "username-timestamp"进行加密,需要后端解密后检验合法性。
加密方式是AES/ECB/Pkcs7
让AI帮我写了如下代码

import CryptoJS from 'crypto-js'

const pwd = 'thisisapassword!'
const test = "张三-1777050437277";
const paddedData = CryptoJS.pad.Pkcs7.pad(CryptoJS.enc.Utf8.parse(test));
const encryptedData = CryptoJS.AES.encrypt(paddedData, pwd, { mode: CryptoJS.mode.ECB });
const encryptedString = encryptedData.toString();
console.log(encryptedString)
// 以其中某个输出为例 U2FsdGVkX18nUMBHmMlAXnrEJ99KBljWfRhokn41hJw=

找了个在线AES加解密工具测试了一下
链接A-https://www.jijie.ink/tool/aes
OK能解出来,妥了,代码提交。

然后后台拿我数据测试,一测,跑过来和我说
“大哥你这加密不对啊我解不出来。”
并且“啪”甩我一个链接
链接B-http://tool.chacuo.net/cryptaes
(在这个网站上,同样的明文和秘钥加密结果如下
BM1KNQSp2yFVmis0GmrS062NfsDo/rZO+Gqzn2CnisU=)
“你自己试试看看。”

然后我就发现,同样的加密模式,同样的填充方式,同样不加偏移,同样输出base64,
以及同样的秘钥和加密文本
为什么这两个网站,加密得到的数据居然不一样?!
而且更离谱的是,明明我都没有加偏移量,
我的代码和链接A,每次加密得到的字符串居然也不一样!?
而且不一样的加密数据居然还能解密出同样的原文。
太迷惑了,AI解决不了我的困扰。

我现在想知道,AES加密,除了加密模式、填充方式、偏移和输出外,还有其他变量能影响加密结果吗?
我又应该如何修改我的代码,能让js的加密在后端正常解密呢?

阅读 845
1 个回答
✓ 已被采纳

以下内容来自 AI:


这个问题在于你对 CryptoJS 的 API 理解存在偏差,以及混淆了 AES 密钥(Key)口令(Password) 的概念。

简单直接的回答:CryptoJS.AES.encrypt() 在传入字符串作为秘钥时,默认执行的是 OpenSSL 兼容的 KDF(密钥派生函数)流程,而不是直接将该字符串作为 AES 密钥。

以下是导致你困惑的三个核心原因:

1. 盐值(Salt)的存在:为什么结果总在变?

当你调用 CryptoJS.AES.encrypt(data, "thisisapassword!") 时,CryptoJS 看到第二个参数是字符串,会认为这是一个口令而非密钥

  • 它会随机生成一个 8 字节的盐值(Salt)
  • 利用这个盐值和你的口令,通过 EVP_BytesToKey 算法计算出真正的 AES 密钥和初始化向量(IV)。
  • 最终输出的 Base64 字符串其实包含了 Salted__ 前缀 + 盐值 + 密文。
  • 结论: 因为盐值是随机的,所以每次加密结果都不同;链接 A 支持这种 OpenSSL 格式,所以能解开;链接 B 默认是纯密文模式,所以解不开。

2. 密钥长度与编码:为什么链接 B 结果不同?

在链接 B 这种标准工具中,它要求你输入的是十六进制(Hex)字符串直接映射的密钥。

  • AES 密钥必须是 16/24/32 字节。
  • 你的字符串 thisisapassword! 正好是 16 字节,但 CryptoJS 默认并没有把它当成原始字节处理,而是走了一遍上面提到的派生流程。

3. 多余的手动填充

CryptoJS 的 AES.encrypt 默认就是 Pkcs7 填充。你代码中手动执行 CryptoJS.pad.Pkcs7.pad(...) 是多此一举,这会导致数据被填充两次,后端解密后会看到末尾带有乱码或填充字符。


如何修改代码以实现前后端一致?

要让前端代码与后端(以及标准工具 B)对接,你需要显式地将密钥定义为字节序列,从而跳过随机盐值的派生流程。

import CryptoJS from 'crypto-js'

// 1. 确保密钥是字节数组格式 (Word arrays)
// 注意:密钥长度必须是 16, 24, 或 32 字节
const key = CryptoJS.enc.Utf8.parse('thisisapassword!'); 

const test = "张三-1777050437277";

// 2. 直接加密,不要手动去 pad
// 传入 WordArray 格式的 key,CryptoJS 就会进入“直接加密模式”而不是“口令模式”
const encrypted = CryptoJS.AES.encrypt(test, key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
});

// 3. 获取密文的 Base64 字符串
const encryptedString = encrypted.toString(); 

console.log(encryptedString); 
// 此时输出将固定为:BM1KNQSp2yFVmis0GmrS062NfsDo/rZO+Gqzn2CnisU=

总结:影响 AES 结果的所有隐性变量

除了你提到的模式、填充、偏移量(IV)外,实际工程中还有以下变量:

  1. 密钥派生函数(KDF): 是否使用了盐值对口令进行了二次哈希(如 PBKDF2, EVP_BytesToKey)。
  2. 编码方式: 密钥和明文是按 UTF-8, Latin1 还是 Hex 转换成字节的?字节对不上,密文一定对不上。
  3. 输出格式: 最终字符串是 Base64 还是 Hex?(这只是展示区别,不影响加密本质)。

建议: 在前后端对接时,永远要求对方提供 Hex 格式的 Key,并明确是否需要 IV。在 ECB 模式下,IV 被忽略,但在更安全的 CBC 模式下,IV 必须保持一致。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题