前端 AES 加密

前端 AES 加密

_

一、问题背景

在开发中,前端需要对接后端的加密 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. 密钥格式必须一致

方面

错误做法

正确做法

密钥类型

直接用字符串

转为 WordArray

字节序

让 CryptoJS 自动处理

手动按大端序拼接

补齐方式

不处理

补 0x00 到 16/24/32 字节

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: encryptStringtimeStamp

  • Response: 后端返回的错误信息


七、总结

  1. 核心问题:前后端密钥处理方式不一致

  2. 解决方案:在 JavaScript 中复刻 Java 的密钥处理逻辑

  3. 关键步骤

    • 字符串 → 字节数组

    • 补 0x00 对齐到 16/24/32 字节

    • 转换为 CryptoJS WordArray

  4. 注意事项

    • 参数顺序必须一致

    • 密钥格式必须一致

    • 使用 WordArray 而非字符串


八、参考资源

TypeScript接口(interface)定义 -interface 2026-01-26
使用 frp + 云服务器实现《星露谷物语》公网联机教程 2026-01-08

评论区