一、问题背景
在开发中,前端需要对接后端的加密 API。后端使用 Java 实现 AES-ECB 加密,前端使用 CryptoJS 库,但遇到了"不正确"的解密失败问题。
原因:前端和后端的密钥处理方式不一致,导致加密结果不同。
二、加密流程
整体流程
原始参数
↓
参数拼接(固定顺序)
↓
Base64 编码
↓
AES-ECB 加密
↓
发送 encryptString + timeStamp
数据示例
// 输入参数
const data = {
sessionID: "abc123",
companyID: "xyz789",
memberID: "user001",
// ...
};
// 时间戳作为密钥
const timeStamp = "1737874521000";
// 拼接后(注意顺序)
const paramString = "abc123xyz789user0011737874521000";
// Base64 编码
const base64String = base64.encode(paramString);
// AES 加密
const encryptString = AES_ECB_Encrypt(base64String, processKey(timeStamp));
三、核心问题:密钥处理
为什么需要 processKey?
AES 加密要求密钥长度必须是 16、24 或 32 字节(对应 AES-128/192/256)。
时间戳字符串如 "1737874521000" 只有 13 字节,不符合标准,需要补齐。
Java 后端的密钥处理
public byte[] processKey(String key) {
// 1. 转为 UTF-8 字节数组
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
// 2. 选择目标长度
int length = keyBytes.length;
int targetLength = 16;
if (length > 16 && length <= 24) {
targetLength = 24;
} else if (length > 24) {
targetLength = 32;
}
// 3. 创建目标数组并补 0x00(Java 默认为 0)
byte[] processedKey = new byte[targetLength];
System.arraycopy(keyBytes, 0, processedKey, 0, Math.min(length, targetLength));
return processedKey;
}
四、前端实现(JavaScript)
完整代码
import CryptoJS from 'crypto-js';
/**
* 处理密钥,与 Java 后端保持一致
* @param {string} key - 原始密钥字符串(如时间戳)
* @returns {CryptoJS.lib.WordArray} - 符合 AES 标准的密钥
*/
function processKey(key) {
// ========== 1. 完全复刻 Java:key.getBytes(StandardCharsets.UTF_8) ==========
let keyBytes = [];
for (let i = 0; i < key.length; i++) {
const charCode = key.charCodeAt(i);
keyBytes.push(charCode & 0xff);
}
const length = keyBytes.length;
// ========== 2. 选择目标长度 ==========
let targetLength = 16;
if (length > 16 && length <= 24) {
targetLength = 24;
} else if (length > 24) {
targetLength = 32;
}
// ========== 3. System.arraycopy + 补 0x00 ==========
const processedKeyBytes = new Array(targetLength).fill(0);
const copyLength = Math.min(length, targetLength);
for (let i = 0; i < copyLength; i++) {
processedKeyBytes[i] = keyBytes[i];
}
// ========== 4. 转换为 CryptoJS WordArray ==========
const words = [];
for (let i = 0; i < processedKeyBytes.length; i += 4) {
words.push(
(processedKeyBytes[i] << 24) |
(processedKeyBytes[i + 1] << 16) |
(processedKeyBytes[i + 2] << 8) |
processedKeyBytes[i + 3]
);
}
return CryptoJS.lib.WordArray.create(words, targetLength);
}
/**
* 加密函数
*/
const getEncodeParams = (data) => {
const timeStamp = Date.now().toString();
// 参数拼接(顺序必须与后端一致)
const paramString =
(data.sessionID || '') +
(data.siteID || '') +
(data.applicationID || '') +
(data.categoryID || '') +
(data.navigatorID || '') +
(data.companyID || '') +
(data.EmployeeID || '') +
(data.memberID || '') +
(data.nameCardID || '') +
timeStamp;
// Base64 编码
const base64String = base64.encode(paramString);
// 处理密钥
const rawKey = timeStamp;
const processedKey = processKey(rawKey);
// AES 加密
const encryptString = CryptoJS.AES.encrypt(base64String, processedKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
}).toString();
return {
timeStamp,
encryptString,
};
};
五、关键要点
1. 参数顺序必须一致
前后端的参数拼接顺序必须完全相同:
// ✅ 正确
const paramString = sessionID + siteID + companyID + memberID + timeStamp;
// ❌ 错误 - 顺序不同
const paramString = memberID + sessionID + companyID + timeStamp;
2. 密钥格式必须一致
3. WordArray 的字节序
// 大端序:高位字节在前
words.push(
(byte[i] << 24) | // 最高位
(byte[i+1] << 16) | // 次高位
(byte[i+2] << 8) | // 次低位
byte[i+3] // 最低位
);
4. 为什么不直接用字符串?
// ❌ 错误 - CryptoJS 会用 Password-based 方式处理
CryptoJS.AES.encrypt(data, "myPassword", {...})
// ✅ 正确 - 直接使用字节数组作为密钥
CryptoJS.AES.encrypt(data, processKey("myPassword"), {...})
CryptoJS 当传入字符串时,会使用类似 EvpKDF 的密钥派生算法,这与 Java 的 SecretKeySpec 行为不同。
六、调试技巧
1. 打印加密结果
console.log('原始密钥:', rawKey);
console.log('处理后密钥:', processedKey.toString());
console.log('加密结果:', encryptString);
console.log('时间戳:', timeStamp);
2. 临时关闭加密测试
// 测试时临时改为 false
const isEncode = false;
3. 对比前后端
在浏览器 Network 标签查看:
Request Payload:
encryptString和timeStampResponse: 后端返回的错误信息
七、总结
核心问题:前后端密钥处理方式不一致
解决方案:在 JavaScript 中复刻 Java 的密钥处理逻辑
关键步骤:
字符串 → 字节数组
补 0x00 对齐到 16/24/32 字节
转换为 CryptoJS WordArray
注意事项:
参数顺序必须一致
密钥格式必须一致
使用 WordArray 而非字符串