1. 项目概述为什么需要模拟前端加密在前后端分离的架构里数据安全传输是基石。我们经常遇到这样的场景前端工程师用 CryptoJS 的 AES/CBC 模式对敏感数据比如登录密码、支付信息进行加密然后通过 HTTP 请求发送给后端。后端 Java 服务则使用 Hutool 这样的工具库进行解密。听起来很标准对吧但问题就出在“联调”和“测试”这两个环节。你作为后端开发接口写好了Hutool 的解密代码也配上了但前端兄弟的页面还没联调好或者你只是想用 Postman 单独测试一下这个加密接口。这时候你发现 Postman 发出去的明文请求后端根本解不开因为后端期待的是密文。于是你不得不去写一段临时的、丑陋的测试代码或者干脆让前端先给你一个加密后的字符串硬编码进去。这既低效又破坏了测试的完整性和真实性。这个项目的核心价值就在这里在 Postman 的预请求脚本Pre-request Script里用 JavaScript 完整复现前端 CryptoJS 的 AES/CBC 加密逻辑。让你在 Postman 里就能模拟出前端加密后的请求体直接发给后端 Hutool 解密接口进行测试。这不仅仅是“能调通”更是保证两端加密解密逻辑严格一致、避免联调扯皮的关键。我见过太多团队因为一个 Padding 模式或者 IV 处理方式的细微差别在联调阶段浪费一两天时间。通过这个实战我们将把整个过程透明化、脚本化实现真正的“无缝对接”。2. 核心思路与方案选型为何是 AES/CBC PKCS7Padding在动手之前我们必须把加密方案的选择讲清楚这是后续一切操作的基础也是很多开发者第一次接触时最容易混淆的地方。2.1 为什么选择 AES/CBC 模式AES高级加密标准是目前对称加密的黄金标准安全性和性能都经过充分验证。而 CBC密码分组链接模式是其最常用的一种工作模式。CBC 模式的工作原理它会将明文分成固定大小的块AES 是 128 位即 16 字节每个明文块在加密前会先与前一个密文块进行异或操作。第一个块则与一个叫做“初始化向量”的随机值进行异或。这个机制使得即使完全相同的明文每次加密也会产生完全不同的密文有效抵御了模式分析攻击。CBC 的核心要求初始化向量一个长度等于分组大小16字节的随机值。它不需要保密但必须不可预测且每次加密都应不同。通常 IV 会随密文一起传输。分组对齐明文长度必须是 16 字节的整数倍。如果不是就需要进行“填充”。选择 CBC 是因为它在安全性和实现复杂度之间取得了很好的平衡并且被 CryptoJS 和 Java JCEJava 加密体系广泛支持是前后端加解密对接中最常见的模式。2.2 为什么填充方案必须是 PKCS7Padding因为明文长度不总是 16 字节的倍数所以需要填充。PKCS#7 是最通用、最标准的填充方案。PKCS7Padding 的规则假设需要填充N个字节那么这N个字节的值就都是N。例如如果最后一个块还差 3 个字节就填充0x03 0x03 0x03。关键点在 Java 的世界里标准库的Cipher类中填充模式的名字叫PKCS5Padding。这是一个历史遗留问题。因为 PKCS#5 标准最初是为 8 字节分组设计的而 PKCS#7 是它的泛化适用于 1-255 字节的分组。对于 AES16字节分组PKCS5Padding和PKCS7Padding在算法上完全等价。所以当你在 Java 中指定AES/CBC/PKCS5Padding时实际执行的就是 PKCS#7 填充。这里就引出了第一个关键对接点CryptoJS 默认使用的就是 PKCS7 填充。因此Java 后端必须使用AES/CBC/PKCS5Padding这个算法字符串才能正确解密。2.3 密钥的格式与处理Base64 还是 Hex密钥是加密解密的根本。AES-128 的密钥是 16 字节AES-192 是 24 字节AES-256 是 32 字节。在实际传输和配置中我们很少直接处理二进制字节而是将其编码为字符串。CryptoJS 的偏好CryptoJS 的AES.encrypt方法通常接受一个“WordArray”对象作为密钥。为了方便我们经常将一个 Hex十六进制字符串或 Base64 字符串传递给它。但关键在于CryptoJS 会把这个字符串当作“密码”并使用它自己的一个基于 OpenSSL 兼容的密钥派生函数EVP_BytesToKey来生成实际用于加密的密钥和 IV。这引入了不必要的复杂性且与 Java 端难以匹配。最佳实践对接关键为了确保两端密钥一致我们应该直接使用原始的密钥字节并将其编码为 Base64 或 Hex 字符串进行存储和传递。在 CryptoJS 中我们可以使用CryptoJS.enc.Base64.parse(base64KeyString)或CryptoJS.enc.Hex.parse(hexKeyString)来将字符串还原为 WordArray。在 Java 中则使用Base64.getDecoder().decode(base64KeyString)或类似的 Hex 解码库来获取字节数组。方案总结为了无缝对接我们约定加密算法AES/CBC/PKCS7Padding(CryptoJS) 对应AES/CBC/PKCS5Padding(Java)。密钥处理双方使用相同编码格式推荐 Base64的同一组密钥字节。IV 处理使用随机生成的 16 字节 IV并将其与密文一起传输通常拼接或单独放在请求头中。3. 环境准备与工具解析工欲善其事必先利其器。我们先来明确这个实战中需要用到的核心工具及其角色。3.1 Postman 与预请求脚本Postman 不仅是 API 测试工具其强大的“预请求脚本”功能让我们能在请求发出前执行 JavaScript 代码。这正是我们模拟前端加密逻辑的舞台。脚本执行环境Postman 的脚本环境基于 Node.js 的沙箱内置了包括CryptoJS在内的多个常用库。这意味着我们无需额外安装可以直接在脚本中引用CryptoJS对象。访问请求数据在预请求脚本中我们可以通过pm.request对象获取即将发出的请求的 URL、头信息、请求体等并对其进行修改。我们的目标就是读取请求体中的明文数据加密后再写回请求体。设置环境变量为了方便管理密钥等敏感或配置信息我们通常将它们存储在 Postman 的环境变量或全局变量中在脚本中通过pm.environment.get和pm.environment.set来存取。3.2 CryptoJS 简介CryptoJS 是一个强大的 JavaScript 加密算法库。在 Postman 中它是内置的我们主要使用其 AES 模块。核心对象CryptoJS.AES核心方法CryptoJS.AES.encrypt(plaintext, key, options)加密。CryptoJS.AES.decrypt(ciphertext, key, options)解密。参数说明plaintext需要加密的明文可以是字符串也可以是CryptoJS.lib.WordArray对象。key密钥可以是字符串、WordArray。强烈建议传递 WordArray 以避免自动密钥派生。options一个配置对象最重要的两个属性是iv初始化向量必须是 WordArray 类型。mode加密模式如CryptoJS.mode.CBC。padding填充模式如CryptoJS.pad.Pkcs7。3.3 Java 后端Hutool 与 Bouncy CastleHutool 是一个 Java 工具类库其hutool-crypto模块对 JDK 的加密解密 API 进行了极佳的封装让代码更简洁。但有时仅靠 JDK 和 Hutool 还不够。Hutool 的 SymmetricCrypto这是 Hutool 进行对称加密的核心类。使用SymmetricCrypto可以很方便地指定算法、密钥和 IV 进行加解密。为什么需要 Bouncy CastleJDK 自带的加密提供者Provider功能有限。一个典型的问题是JDK 默认的AES/CBC/PKCS5Padding实现在解密时如果密文长度不正确例如不是分组的整数倍可能会抛出异常而不是先解密再验证填充。而 Bouncy CastleBC提供者的行为更符合通用标准兼容性更好尤其是在处理来自其他语言或库如 CryptoJS的密文时。引入 BC 能极大提高对接成功率。Bouncy Castle 的引入只需在项目中加入 BC 的依赖如org.bouncycastle:bcprov-jdk15to18并在代码中静态注册Security.addProvider(new BouncyCastleProvider())Hutool 会自动优先使用 BC 提供者。实操心得一依赖陷阱如果你发现 Hutool 解密总是报BadPaddingException或其他奇怪错误而 CryptoJS 加密看起来没问题第一件事就是检查是否引入了 Bouncy Castle 并正确注册。这能解决 80% 的跨语言解密兼容性问题。Maven 依赖记得选对版本与你的 JDK 版本匹配。4. Postman 预请求脚本加密实战现在我们进入核心实操环节。我将一步步展示如何在 Postman 中编写预请求脚本将请求体中的 JSON 数据自动加密。4.1 设置 Postman 环境变量首先我们把密钥和 IV 等配置信息放在环境变量里这样脚本可以读取也便于在不同环境测试、生产间切换。在 Postman 右上角点击眼睛图标管理环境。创建一个新环境比如叫 “AES-CBC-Test”。添加以下变量aes_key_base64: 你的 AES 密钥的 Base64 编码字符串。例如一个 16 字节的密钥u/Guq6r1MBq31z8Twu6WUA(可以通过在线工具生成)。aes_iv_base64: 你的初始化向量的 Base64 编码字符串。注意为了测试可重复性我们先使用固定 IV。在生产脚本中IV 应该每次随机生成。例如AAAAAAAAAAAAAAAAAAAAAA(16个A的Base64)。在发送请求前确保在右上角下拉框中选中了 “AES-CBC-Test” 这个环境。4.2 编写预请求脚本在你的 Postman 请求中切换到 “Pre-request Script” 标签页。粘贴以下脚本// 1. 从环境变量中获取密钥和IV (Base64格式) const keyBase64 pm.environment.get(aes_key_base64); const ivBase64 pm.environment.get(aes_iv_base64); // 2. 将Base64字符串转换为CryptoJS可识别的WordArray对象 // 这是关键步骤直接parse Base64确保密钥字节原样传递。 const key CryptoJS.enc.Base64.parse(keyBase64); const iv CryptoJS.enc.Base64.parse(ivBase64); // 3. 获取当前请求的请求体假设是JSON格式的raw body const requestBody pm.request.body; if (requestBody requestBody.mode raw requestBody.raw) { try { const rawData requestBody.raw; // 你可以先解析JSON对特定字段加密这里演示加密整个body字符串 // 注意实际场景可能只加密body中的某个字段如password const plainText rawData; // 4. 使用CryptoJS进行AES/CBC/PKCS7加密 const encrypted CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 // 明确指定PKCS7填充 }); // 5. 加密结果是一个CipherParams对象我们需要获取密文的Base64字符串形式 const cipherTextBase64 encrypted.ciphertext.toString(CryptoJS.enc.Base64); // 6. 将加密后的密文更新回请求体 // 通常后端期望接收一个JSON对象里面包含密文字段例如{data: 加密后的Base64字符串} const newBody { encryptedData: cipherTextBase64, // 如果IV不是固定的也需要传给后端例如 // iv: ivBase64 }; // 将新的JSON对象设置为请求体 pm.request.body.update({ mode: raw, raw: JSON.stringify(newBody) }); // 可选修改Content-Type头为application/json pm.request.headers.upsert({ key: Content-Type, value: application/json }); console.log(加密成功密文(Base64):, cipherTextBase64); console.log(新的请求体:, JSON.stringify(newBody)); } catch (error) { console.error(加密过程中发生错误:, error); // 可以选择不修改请求体让请求以明文发出或者抛出错误 } } else { console.warn(请求体不是raw格式或为空跳过加密。); }脚本逐行解析与注意事项第2步密钥转换CryptoJS.enc.Base64.parse是灵魂。它确保了我们将 Base64 编码的密钥字节直接作为加密密钥使用而不是让 CryptoJS 把字符串当成密码去派生。这是与 Java 端对齐的最关键一步。第4步加密选项我们显式指定了mode和padding避免依赖 CryptoJS 可能变化的默认配置让行为更确定。第6步请求体构造这里演示了将整个原始请求体字符串加密。但更常见的场景是只加密 JSON 中的某个敏感字段。例如你的请求体原是{username:zhangsan,password:123456}你只加密password字段。脚本需要先JSON.parse(rawData)然后加密password的值再组装新的 JSON。这需要根据你的后端接口契约来调整。IV 的传输如果使用随机 IV必须将 IV 也传给后端因为解密时需要相同的 IV。通常有两种方式1) 将 IV 的 Base64 字符串也放在 JSON 请求体中2) 将 IV 放在一个自定义的 HTTP 头里如X-IV。后端需要从对应位置取出。实操心得二调试技巧Postman 的console.log输出可以在底部的 “Console” 标签页查看。务必在编写脚本时打开 Console观察密钥、IV、明文、密文的转换过程确保每一步都符合预期。特别是检查key和iv转换后的 WordArray 的sigBytes属性确认是 16128位或 32256位。4.3 测试脚本效果在请求的 “Body” 标签页选择 “raw” 和 “JSON”输入你的明文 JSON例如{username: testUser, password: mySecretPassword}。确保预请求脚本已保存。点击 “Send” 发送请求。在 Postman 的 “Console” (View - Show Postman Console) 中你应该能看到 “加密成功” 的日志。切换到 “Request” 部分查看实际发出的请求你会发现 Body 已经变成了类似{encryptedData: U2FsdGVkX1/...很长一串Base64...}的形式。至此Postman 端的加密模拟已经完成。你发出的每一个请求其请求体都已经是前端 CryptoJS 加密后的标准格式。5. Java (Hutool) 后端解密实现前端模拟好了后端必须能正确解密。我们使用 Hutool 来简化代码并引入 Bouncy Castle 提升兼容性。5.1 项目依赖准备在你的pom.xml中添加以下依赖dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.16/version !-- 请使用最新稳定版 -- /dependency dependency groupIdorg.bouncycastle/groupId artifactIdbcprov-jdk15to18/artifactId version1.72/version !-- 请使用最新稳定版 -- /dependency5.2 解密工具类编写创建一个AesCbcUtil工具类封装解密逻辑。import cn.hutool.core.codec.Base64; import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.symmetric.AES; import org.bouncycastle.jce.provider.BouncyCastleProvider; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.Security; public class AesCbcUtil { static { // 静态注册BouncyCastle提供者确保优先使用 Security.addProvider(new BouncyCastleProvider()); } /** * 使用Hutool AES工具解密 (CBC模式, PKCS7Padding) * * param base64Key Base64编码的密钥 * param base64Iv Base64编码的初始化向量 * param base64Data Base64编码的密文 * return 解密后的明文字符串 */ public static String decryptWithHutool(String base64Key, String base64Iv, String base64Data) { // 1. 解码Base64得到字节数组 byte[] keyBytes Base64.decode(base64Key); byte[] ivBytes Base64.decode(base64Iv); byte[] encryptedBytes Base64.decode(base64Data); // 2. 构建AES对象指定CBC模式和PKCS5Padding对应PKCS7 // Hutool 内部会使用我们注册的BouncyCastle提供者 AES aes new AES(Mode.CBC, Padding.PKCS5Padding, new SecretKeySpec(keyBytes, AES), new IvParameterSpec(ivBytes)); // 3. 解密并返回字符串 byte[] decryptedBytes aes.decrypt(encryptedBytes); return new String(decryptedBytes, StandardCharsets.UTF_8); } /** * 解密HTTP请求体假设请求体为JSON包含encryptedData字段 * 这是一个Spring MVC Controller中的示例方法 */ /* PostMapping(/decrypt-endpoint) public ResponseEntity? handleEncryptedRequest(RequestBody MapString, String requestBody) { String encryptedDataBase64 requestBody.get(encryptedData); // 从配置或环境变量获取密钥和IV String keyBase64 u/Guq6r1MBq31z8Twu6WUA; String ivBase64 AAAAAAAAAAAAAAAAAAAAAA; try { String decryptedText decryptWithHutool(keyBase64, ivBase64, encryptedDataBase64); // 将解密后的文本解析为实际的业务对象 // ObjectMapper mapper new ObjectMapper(); // MyRequestDto dto mapper.readValue(decryptedText, MyRequestDto.class); // ... 处理业务逻辑 ... return ResponseEntity.ok(解密成功: decryptedText); } catch (Exception e) { log.error(解密失败, e); return ResponseEntity.badRequest().body(解密失败: e.getMessage()); } } */ }代码关键点解析静态注册 BouncyCastle在类加载时注册确保整个应用生命周期内生效。这是避免BadPaddingException等兼容性问题的关键。Hutool AES 构造器new AES(Mode.CBC, Padding.PKCS5Padding, ...)这里明确指定了模式和填充。注意是PKCS5Padding与 CryptoJS 的Pkcs7对应。密钥和 IV 规范使用SecretKeySpec和IvParameterSpec包装解码后的字节数组这是 JCE 的标准做法。解密流程aes.decrypt()方法接收密文字节数组返回明文字节数组。我们再将其转为 UTF-8 字符串。5.3 处理只加密部分字段的场景很多时候我们只加密请求体中的某个字段如password。此时后端接收到的 JSON 结构可能是{ username: testUser, encryptedPassword: U2FsdGVkX1/..., iv: AAAAAAAAAAAAAAAAAAAAAA }后端的解密逻辑就需要调整// 在Controller方法中 String username requestBody.get(username); String encryptedPasswordBase64 requestBody.get(encryptedPassword); String ivBase64 requestBody.get(iv); // 如果IV是前端随机生成并传来的 String decryptedPassword decryptWithHutool(keyBase64, ivBase64, encryptedPasswordBase64); // 然后使用 username 和 decryptedPassword 进行后续逻辑6. 联调与常见问题排查实录即使按照上述步骤操作第一次对接也难免遇到问题。下面是我在多次对接中总结的“排坑指南”。6.1 问题一解密失败报错javax.crypto.BadPaddingException: Given final block not properly padded可能原因 1密钥不一致。这是最常见的原因。排查在 Postman Console 和 Java 后端日志中分别打印出密钥和 IV 的Base64 字符串和字节数组长度。确保两者完全一致。检查 Postman 环境变量名是否正确是否有空格等不可见字符。技巧在 Postman 脚本开头加console.log(Key Base64:, keyBase64, Length:, CryptoJS.enc.Base64.parse(keyBase64).sigBytes);。在 Java 端加log.info(Key Bytes Length: {}, Base64.decode(base64Key).length);。AES-128 长度应为 16AES-256 应为 32。可能原因 2IV 不一致或未传递。排查同上检查 IV 的 Base64 字符串和字节长度必须是 16。确认前端是否将 IV 传给了后端后端是否从正确的位置请求体或 Header取出了 IV。可能原因 3加密模式或填充模式不匹配。排查确认 CryptoJS 使用了CBC模式和Pkcs7填充。确认 Java Hutool 使用了CBC模式和PKCS5Padding。一个字母都不能错。可能原因 4密文在传输过程中被修改或编码问题。排查检查 Postman 发出的请求体中的encryptedData字符串和 Java 后端通过requestBody.get(“encryptedData”)收到的字符串是否完全一致。特别注意 URL 编码问题。如果密文作为 URL 参数传递需要确保没有发生额外的编码/解码。最佳实践是始终通过 Request Body 传输密文并使用 Base64 编码。6.2 问题二解密出的明文是乱码可能原因字符编码不一致。排查CryptoJS 加密的明文是字符串它默认使用 UTF-8 吗实际上CryptoJS 的encrypt方法接受字符串时会使用一种特定的编码通常是 Latin1 或 UTF-8但行为可能因版本而异。最稳妥的方式是在 CryptoJS 加密前将字符串明确转换为CryptoJS.enc.Utf8.parse(plainText)得到一个 WordArray 再加密。在 Java 端解密后使用new String(decryptedBytes, StandardCharsets.UTF_8)指定 UTF-8 解码。修正方案 Postman 脚本// 将明文字符串转为UTF-8的WordArray const plainTextWordArray CryptoJS.enc.Utf8.parse(plainTextString); const encrypted CryptoJS.AES.encrypt(plainTextWordArray, key, { iv: iv, mode: CryptoJS.mode.CBC });6.3 问题三Postman 脚本报错CryptoJS is not defined可能原因Postman 的沙箱环境问题极少数情况下 CryptoJS 对象未加载。排查在脚本最开头加console.log(typeof CryptoJS, typeof CryptoJS.AES)看看。解决重启 Postman或者检查 Postman 版本。通常内置的 CryptoJS 是稳定的。6.4 问题四Java 端报java.security.InvalidKeyException: Illegal key size可能原因使用了 AES-256但 JDK 默认的受限策略文件限制了密钥长度。排查确认密钥长度是 32 字节256位。解决推荐方案换用 AES-12816字节密钥安全性对大多数场景已足够。安装 Java 无限制强度管辖策略文件JCE。去 Oracle 官网下载对应你 JDK 版本的local_policy.jar和US_export_policy.jar替换掉$JAVA_HOME/jre/lib/security/下的同名文件。6.5 问题速查表现象可能原因排查步骤BadPaddingException1. 密钥/IV不一致2. 密文被破坏3. 模式/填充不匹配1. 对比两端密钥/IV的Base64和长度2. 对比原始密文和接收到的密文3. 确认两端算法字符串解密结果乱码字符编码问题1. CryptoJS端使用Utf8.parse2. Java端使用UTF_8解码InvalidKeyExceptionJDK策略限制1. 检查密钥长度2. 安装JCE无限制策略文件Postman脚本不执行脚本语法错误/环境未选1. 查看Postman Console错误信息2. 确认正确环境被选中7. 安全增强与生产环境建议上面的示例为了演示清晰使用了固定的 IV。但在生产环境中这是不安全的。CBC 模式要求每次加密使用随机、不可预测的 IV。7.1 在 Postman 中实现随机 IV修改预请求脚本每次请求生成随机 IV// 生成16字节的随机IV (CryptoJS.lib.WordArray) const randomIv CryptoJS.lib.WordArray.random(16); const ivBase64 CryptoJS.enc.Base64.stringify(randomIv); // 将本次使用的IV存入环境变量或局部变量以便放在请求中传给后端 pm.environment.set(current_random_iv, ivBase64); // 或者直接构造在请求体中 const newBody { encryptedData: cipherTextBase64, iv: ivBase64 // 将随机IV传给后端 };7.2 后端适配随机 IV后端解密时不再从固定配置读取 IV而是从请求中提取// 从请求体Map中获取IV String ivBase64FromRequest requestBody.get(iv); String decryptedText decryptWithHutool(keyBase64, ivBase64FromRequest, encryptedDataBase64);7.3 密钥管理绝对不要将密钥硬编码在代码或 Postman 脚本中。Postman 环境变量中的密钥应仅为测试使用。生产环境的密钥应通过安全的配置中心如 Apollo, Nacos或 KMS 服务获取。可以考虑将 Postman 的集合和环境变量导出为 JSON 文件但务必通过.gitignore排除或加密存储避免密钥泄露。7.4 考虑使用更现代的模式对于新项目可以考虑比 CBC 更安全的模式如GCM (Galois/Counter Mode)。GCM 同时提供加密和认证功能可以防止密文被篡改。不过CryptoJS 和 Hutool 对 GCM 的支持度可能不如 CBC 广泛对接时需要额外注意。如果选择 GCM需要处理“认证标签”的传递。整个流程走下来从 Postman 脚本的编写到 Hutool 后端的对接核心就在于细节的匹配密钥字节、IV、算法模式、填充方式、字符编码。只要这五个要素在前后端完全一致加解密过程就能像齿轮一样严丝合缝地转动。这个实战方案不仅能用于调试其脚本和工具类稍加改造也可以作为前端加密 SDK 和后端解密工具的标准参考实现确保整个数据链路的安全与可靠。