1. 项目概述当接口测试遇上国密算法做接口测试和调试Postman几乎是每个开发者和测试工程师的标配工具。我们用它来构造请求、查看响应、管理环境变量流程顺畅得就像用筷子夹菜。但最近两年我手头的项目越来越多地涉及到一个新需求接口数据的加解密尤其是国密算法SM2、SM3、SM4的应用。甲方爸爸一句“为了符合安全规范”原本明文的{“username”: “admin”}瞬间变成了长达几百个字符、面目全非的密文字符串。这时候传统的Postman玩法就有点捉襟见肘了。你不可能每次都先写一段加密代码把数据加密后再手动粘贴到Postman的Body里更不可能在收到一堆乱码似的响应后再打开另一个IDE去写解密代码。流程被彻底打断调试效率断崖式下跌。这个项目要解决的就是如何在Postman这个“前端”工具里无缝集成AES、SM3、SM4这些加解密逻辑让接口调试回归它应有的流畅。这不仅仅是写几个脚本更是对Postman预请求脚本Pre-request Script和测试脚本Tests Script能力的深度挖掘构建一套可复用、易管理的本地加解密工作流。2. 核心思路在请求前后自动完成“翻译”工作我的核心思路很直接把Postman想象成一个智能代理。在请求发出前它能自动把我要发送的明文数据按照接口要求的算法和密钥“翻译”加密成密文在收到响应后它又能自动把服务器返回的密文“翻译”解密成我能看懂的明文。整个过程对用户透明我只需要关心业务数据本身。实现这个目标主要依赖Postman的两个核心功能预请求脚本 (Pre-request Script)在请求被发送到服务器之前执行。这里是实现请求体加密和签名生成的天然场所。测试脚本 (Tests Script)在收到服务器响应后执行。这里是实现响应体解密和签名验证的最佳位置。而加解密逻辑本身我们将用JavaScript来编写。Postman的脚本环境基于Node.js但并非完整的Node环境它内置了一个功能强大的pm对象来提供各种能力。对于AES这种国际通用算法我们可以直接使用CryptoJS这个强大的库Postman沙箱环境已内置。但对于国密算法SM2/SM3/SM4Postman并没有原生支持这就需要我们“自带干粮”通过一些技巧引入第三方JavaScript实现。整个方案的设计追求的是配置化和可复用性。我会把算法类型、密钥、偏移量(IV)、工作模式、填充方式等参数都配置在Postman的环境变量或全局变量里。这样同一套脚本就能通过修改变量值轻松适配不同接口、不同项目的加解密需求而不是为每个接口都写死一套代码。3. 环境与工具准备打好地基工欲善其事必先利其器。在开始写脚本之前我们需要把环境和材料准备好。3.1 核心库的获取与引入对于AES我们很幸运。Postman的沙箱环境默认内置了CryptoJS库的一部分常用模块。你可以直接在预请求脚本或测试脚本里使用CryptoJS.AES,CryptoJS.enc.Utf8,CryptoJS.enc.Base64等对象无需任何额外操作。这是最省心的部分。麻烦在于国密算法。Postman没有内置SM系列算法的实现。因此我们需要找到纯JavaScript实现的国密算法库并将其代码“植入”到Postman中。我经过一番搜寻和测试找到了sm-crypto这个比较成熟的库。它支持SM2、SM3、SM4且代码兼容性较好。但是你不能直接在Postman脚本里写require(‘sm-crypto’)因为Postman的沙箱环境不支持直接引入本地npm模块。解决方法是将库的源码“内联”进来。具体操作如下访问sm-crypto的GitHub仓库或通过npm获取其UMD格式的浏览器版本通常是一个单独的.js文件比如sm-crypto.min.js。用文本编辑器打开这个js文件复制其全部内容。在Postman中创建一个新的请求或者在你准备用于加解解的集合Collection的“Pre-request Scripts”标签页里将复制的整个js文件内容粘贴进去。但注意不能直接裸露在脚本里需要用eval()函数来执行它或者将其赋值给一个变量。更优雅的方式是利用Postman的pm.globals.set功能将这段冗长的代码设置为一个全局变量。我采用的方法是将sm-crypto的源码压缩成一行可以使用在线工具然后将其设置为一个全局变量。在集合的预请求脚本中我这样初始化// 在集合级别的Pre-request Script中初始化国密算法库 if (!pm.globals.has(smCryptoLib)) { // 这里是一个超长的字符串是sm-crypto.min.js压缩后的单行代码 const smCryptoCode (function(){.../* 这里是完整的sm-crypto.min.js代码 */...})();; try { eval(smCryptoCode); // 假设sm-crypto库挂载到了全局window或global对象我们将其关键方法提取出来存为全局变量 pm.globals.set(sm2, JSON.stringify(sm2)); pm.globals.set(sm3, sm3); // sm3可能是一个函数 pm.globals.set(sm4, JSON.stringify(sm4)); console.log(国密算法库初始化成功。); } catch (e) { console.error(初始化国密算法库失败, e); } }注意这种方法虽然可行但操作略显繁琐且每次打开Postman都可能需要初始化。更稳定的做法是将这些通用脚本保存在集合的“Pre-request Script”中确保每个属于该集合的请求在执行前都已加载了必要的库。另外直接eval长字符串可能存在性能问题但对于测试调试场景通常可以接受。3.2 关键参数的环境变量配置将可变参数配置化是提升脚本复用性的关键。我通常在Postman中创建一个专门的环境Environment例如叫做“国密测试环境”并在其中定义以下变量变量名示例值说明encrypt_algorithmSM4加密算法可选AES,SM4encrypt_modeCBC加密模式如ECB,CBC,GCMencrypt_paddingPKCS5Padding填充方式如PKCS5Padding,PKCS7Padding,NoPaddingencrypt_key0123456789ABCDEF0123456789ABCDEF加密密钥Hex或Base64格式需与算法匹配encrypt_ivABCDEFGHIJKLMNOP加密偏移量CBC等模式需要Hex或Base64格式sign_algorithmSM3签名算法可选SM3,MD5,SHA256need_decrypt_responsetrue布尔值标记是否需要自动解密响应体这样当我要测试另一个使用AES-256-CBC的接口时只需要在环境里把encrypt_algorithm改成AESencrypt_key换成对应的AES密钥即可脚本代码完全不用动。4. 核心脚本编写请求加密与响应解密准备好了库和配置接下来就是编写核心的脚本逻辑。我会分请求加密和响应解密两部分来写代码会包含详细的注释。4.1 预请求脚本实现请求体加密与签名这个脚本会在请求发送前执行。它的主要任务是获取当前请求的原始数据明文。根据环境变量配置的算法和参数对其进行加密。将加密后的密文设置为新的请求体。可选计算请求体的签名并添加到请求头中。假设我们的接口要求请求体是JSON格式且整个JSON字符串需要被加密。以下是脚本示例// Pre-request Script: 对请求体进行加密 const algorithm pm.environment.get(encrypt_algorithm); const key pm.environment.get(encrypt_key); const iv pm.environment.get(encrypt_iv); const mode pm.environment.get(encrypt_mode); const padding pm.environment.get(encrypt_padding); // 1. 获取原始请求数据 let rawBody {}; try { // 支持JSON和form-data格式的请求体获取 if (pm.request.body pm.request.body.raw) { const bodyRaw pm.request.body.raw; if (bodyRaw) { rawBody JSON.parse(bodyRaw); } } } catch (e) { console.log(请求体非JSON或为空按原始文本处理。); // 如果是纯文本可以按字符串处理 // rawBody pm.request.body.raw; } // 将原始对象转换为JSON字符串作为待加密的明文 const plainText JSON.stringify(rawBody); console.log(待加密明文, plainText); let encryptedData ; let encryptedBase64 ; // 2. 根据配置的算法选择加密方式 if (algorithm AES) { // 使用CryptoJS进行AES加密 // 注意CryptoJS的密钥和IV通常需要处理成WordArray对象 const keyWA CryptoJS.enc.Hex.parse(key); // 假设key是Hex格式 const ivWA CryptoJS.enc.Utf8.parse(iv); // 假设iv是UTF-8字符串 let encrypted; if (mode CBC) { encrypted CryptoJS.AES.encrypt(plainText, keyWA, { iv: ivWA, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 // CryptoJS中Pkcs7对应PKCS5Padding }); } else if (mode ECB) { encrypted CryptoJS.AES.encrypt(plainText, keyWA, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }); } // 将加密结果转换为Base64字符串 encryptedBase64 encrypted.toString(); encryptedData encryptedBase64; console.log(AES加密结果(Base64), encryptedBase64); } else if (algorithm SM4) { // 使用预先加载的sm4库进行加密 // 注意这里需要从全局变量中获取之前初始化的sm4对象 const sm4 JSON.parse(pm.globals.get(sm4)); // 假设密钥和IV是Hex字符串且SM4-CBC模式 if (mode CBC) { // sm-crypto 中sm4.encrypt方法参数顺序明文密钥配置项 encryptedData sm4.encrypt(plainText, key, { iv: iv, mode: cbc, outputEncoding: base64 // 输出base64 }); encryptedBase64 encryptedData; console.log(SM4加密结果(Base64), encryptedBase64); } } // 3. 更新请求体为加密后的密文 // 通常接口接收的是Base64字符串或Hex字符串 if (encryptedBase64) { // 将请求体替换为加密后的密文注意格式可能是JSON包装 const newBody { data: encryptedBase64 // 有些接口要求密文放在特定的字段里如data或encryptedData // timestamp: Date.now(), // 还可以同时添加时间戳等字段 }; // 修改请求体 pm.request.body.update({ mode: raw, raw: JSON.stringify(newBody) }); console.log(已更新请求体为密文。); } // 4. 可选生成签名并添加到请求头 const signAlgo pm.environment.get(sign_algorithm); if (signAlgo SM3) { const sm3 pm.globals.get(sm3); // 假设sm3是一个函数 // 签名原文可以是“密文时间戳其他固定字符串”等根据接口规则来 const signString encryptedBase64 Date.now(); const signature sm3(signString); // 计算SM3哈希值作为签名 // 将签名添加到请求头 pm.request.headers.add({ key: X-Signature, value: signature }); }4.2 测试脚本实现响应体解密与验证请求发出后我们会在Tests标签页的脚本里处理响应。主要任务是检查响应状态和内容。根据环境变量判断是否需要解密。从响应中提取密文数据并进行解密。将解密后的明文输出到Postman控制台或设置为环境变量便于查看。// Tests Script: 对响应体进行解密 const needDecrypt pm.environment.get(need_decrypt_response) true; if (!needDecrypt) { console.log(当前环境配置为不解密响应。); return; } // 1. 获取响应文本 const responseBody pm.response.text(); console.log(原始响应, responseBody); let jsonResponse; try { jsonResponse JSON.parse(responseBody); } catch (e) { console.error(响应不是有效的JSON格式无法解密。); return; } // 2. 假设接口返回的数据结构为 { code: 0, data: 加密的Base64字符串 } const encryptedBase64FromResponse jsonResponse.data; if (!encryptedBase64FromResponse) { console.log(响应中未找到加密数据字段data。); return; } const algorithm pm.environment.get(encrypt_algorithm); const key pm.environment.get(encrypt_key); const iv pm.environment.get(encrypt_iv); const mode pm.environment.get(encrypt_mode); let decryptedText ; // 3. 根据算法解密 if (algorithm AES) { const keyWA CryptoJS.enc.Hex.parse(key); const ivWA CryptoJS.enc.Utf8.parse(iv); let decrypted; if (mode CBC) { decrypted CryptoJS.AES.decrypt(encryptedBase64FromResponse, keyWA, { iv: ivWA, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); } // 将解密结果从CryptoJS的WordArray对象转换为UTF-8字符串 decryptedText decrypted.toString(CryptoJS.enc.Utf8); console.log(AES解密结果, decryptedText); } else if (algorithm SM4) { const sm4 JSON.parse(pm.globals.get(sm4)); if (mode CBC) { decryptedText sm4.decrypt(encryptedBase64FromResponse, key, { iv: iv, mode: cbc, inputEncoding: base64, outputEncoding: utf8 // 输出utf8字符串 }); console.log(SM4解密结果, decryptedText); } } // 4. 将解密后的明文设置为环境变量方便在后续请求或界面中引用 if (decryptedText) { try { const parsedData JSON.parse(decryptedText); pm.environment.set(decrypted_response, JSON.stringify(parsedData, null, 2)); console.log(响应解密成功已存入环境变量 decrypted_response。); // 你也可以直接美化输出到控制台 // console.log(解密后的JSON对象, parsedData); } catch (e) { // 如果解密后不是JSON直接存文本 pm.environment.set(decrypted_response, decryptedText); console.log(响应解密成功非JSON已存入环境变量。); } } else { console.error(解密失败解密文本为空。); } // 5. 可选验证响应签名 // 假设响应头里有签名 X-Res-Signature const responseSignature pm.response.headers.get(X-Res-Signature); if (responseSignature) { const signAlgo pm.environment.get(sign_algorithm); if (signAlgo SM3) { const sm3 pm.globals.get(sm3); // 验证签名通常是对响应体原文或特定字段进行哈希计算然后与签名对比 const dataToVerify encryptedBase64FromResponse; // 这里用密文验证具体规则依接口而定 const calculatedHash sm3(dataToVerify); if (calculatedHash responseSignature) { console.log(响应签名验证通过。); } else { console.error(响应签名验证失败); } } }5. 高级技巧与实战优化上面的脚本已经能跑通基本流程但在实际项目中你会遇到各种边界情况和性能需求。下面分享几个我踩过坑后总结的优化技巧。5.1 动态密钥与密钥派生有些安全要求高的场景不会使用固定的静态密钥。可能会采用动态密钥协商或者基于固定密钥和请求参数如时间戳、流水号派生出一个本次请求使用的会话密钥。实现思路你可以在预请求脚本中利用CryptoJS或者引入的库进行密钥派生。例如使用HMAC-SHA256基于主密钥和某个随机数生成一次性的加密密钥。// 示例动态生成AES密钥简化版 const masterKey pm.environment.get(master_key); // 主密钥 const nonce Date.now().toString(); // 随机数实践中应用更安全的随机数 const sessionKey CryptoJS.HmacSHA256(nonce, masterKey).toString().substring(0, 32); // 派生出一个32字节的密钥 // 然后将sessionKey用于本次请求的加密并将nonce放在请求头或体里传给服务端 pm.request.headers.add({ key: X-Nonce, value: nonce }); // 更新用于本次加密的密钥变量 pm.environment.set(temp_session_key, sessionKey); // 临时存一下解密时要用在测试脚本中你需要用同样的算法和nonce重新派生出相同的sessionKey来解密响应。5.2 处理多种数据格式和嵌套加密接口设计千奇百怪。有的不是加密整个JSON字符串而是只加密JSON中的某个特定字段如password字段。有的甚至要求先对字段A加密然后将加密结果和字段B一起组成新JSON再整体加密一次嵌套加密。应对策略这就需要更精细的脚本逻辑。你需要先解析原始请求体定位到需要加密的字段对其进行加密然后替换原字段的值最后再序列化整个对象。这涉及到深拷贝和对象操作。// 示例只加密特定字段 function encryptField(obj, fieldPath, key, iv) { // fieldPath 例如 user.password const keys fieldPath.split(.); let current obj; for (let i 0; i keys.length - 1; i) { current current[keys[i]]; } const lastKey keys[keys.length - 1]; if (current[lastKey]) { const encrypted CryptoJS.AES.encrypt(current[lastKey], key, { iv: iv }).toString(); current[lastKey] encrypted; } return obj; } // 在预请求脚本中使用 const rawBody JSON.parse(pm.request.body.raw); const processedBody encryptField(rawBody, user.password, keyWA, ivWA); pm.request.body.update({ mode: raw, raw: JSON.stringify(processedBody) });5.3 性能优化与脚本管理当你在一个集合Collection里有很多需要加解密的请求时把完整的sm-crypto库代码和加解密函数复制到每个请求的脚本里会非常臃肿且难以维护。最佳实践集合级脚本将通用的库初始化代码和加解密核心函数写在集合Collection的“Pre-request Scripts”和“Tests”标签页里。这样集合下的所有请求都能共享这些函数和变量。外部代码片段对于极其复杂的通用函数可以将其保存为单独的JavaScript文件。在Postman中虽然不能直接require但你可以手动将文件内容复制到一个全局变量中或者利用Postman的“脚本”功能较新版本支持导入/导出进行管理。模块化思维在集合脚本中将加密、解密、签名、验签等功能封装成独立的函数例如encryptData(plainText, algorithm, key, iv)和decryptData(cipherText, algorithm, key, iv)。在每个具体请求的脚本里只需要调用这些函数并传入具体参数即可大大简化了单个请求的脚本逻辑。6. 常见问题排查与调试心得在实际集成过程中你肯定会遇到各种报错和异常。下面是我总结的一些常见问题及其解决方法。6.1 典型错误与解决方案问题现象可能原因排查步骤与解决方案控制台报错CryptoJS is not definedPostman内置的CryptoJS模块在某些版本或沙箱环境下可能加载不全。1. 检查代码拼写是否正确。2. 尝试使用全小写cryptojs某些版本别名。3. 最保险的方法在集合脚本中手动引入一个CDN上的CryptoJS如同引入sm-crypto一样。sm4.encrypt报错 “input is not string”传递给sm4.encrypt的参数类型不对。sm-crypto库通常要求明文和密钥是字符串。1. 确认你的明文plainText是字符串类型。如果是对象先用JSON.stringify转换。2. 确认密钥key是Hex格式或Base64格式的字符串且长度符合SM4要求16字节Hex字符串为32字符。解密后得到乱码1. 加解密使用的密钥、IV、模式、填充方式不一致。2. 编码问题。加密时输入是UTF-8解密时输出却按其他编码解析。3. 密文在传输或处理中被修改如多余的换行、空格。1.逐项核对确保服务端和Postman脚本在算法、模式、填充、密钥、IV上完全一致。一个字符都不能差。2.检查编码在CryptoJS中确保toString(CryptoJS.enc.Utf8)解密。在sm-crypto中配置好inputEncoding和outputEncoding。3.净化密文打印出收到的密文字符串与服务端日志对比看是否有不可见字符。签名验证失败1. 签名原文的拼接规则不一致。2. 哈希算法或编码不一致。3. 请求/响应体在签名后又被修改如空格、排序。1.严格对照接口文档确认签名原文是否包含所有必要字段如密文、时间戳、随机数字段顺序是否固定是否经过URL编码等。2.本地模拟用同样的算法和原文在Node.js或浏览器环境写一个独立脚本计算签名与Postman结果对比定位问题。引入的第三方库代码执行报语法错误复制的库代码可能包含Postman沙箱环境不支持的语法如ES6高级特性。1. 寻找该库的ES5版本或UMD打包版本兼容性更好。2. 使用在线工具如Babel将库代码转译为ES5语法后再复制进来。6.2 Postman脚本调试技巧善用ConsolePostman内置的控制台View - Show Postman Console是调试的生命线。所有console.log()和错误信息都会在这里输出。务必在关键步骤获取变量、加密前、加密后、解密前、解密后打印出中间结果。使用pm.*APIpm.environment.get/set,pm.globals,pm.request,pm.response这些对象提供了强大的交互能力。例如pm.request.headers可以查看和修改请求头这在调试签名问题时非常有用。分步测试不要试图一次性写完所有功能。先在一个简单的请求上测试AES加密解密。通了之后再引入国密库测试SM4。最后再集成签名功能。每一步都确保输入输出符合预期。环境变量快照在调试复杂问题时将关键的环境变量值如密钥、IV通过console.log打印出来与服务器端配置进行比对可以快速排除配置错误。模拟服务器端如果条件允许用Python、Java或Node.js写一个最简单的服务器端加解密程序用已知的明文、密钥进行加密然后将密文拿到Postman里用同样的参数解密这样可以彻底隔离问题确定是Postman脚本问题还是服务端问题。7. 总结与扩展思路通过这一套组合拳我们成功地将国密和国际标准的加解密能力深度集成到了Postman这个前端调试工具中。它带来的效率提升是巨大的你再也不用在代码编辑器和Postman之间反复横跳所有加解密操作都在后台静默完成你的注意力可以完全集中在接口的业务逻辑和数据本身上。这套方案的扩展性也很强。基于这个框架你可以轻松地集成更多算法如DES、3DES、RSA等只需要找到对应的JS库并用类似方法引入。实现更复杂的流程如先压缩再加密先签名再加密等组合安全策略。构建自动化测试结合Postman的Collection Runner或Newman命令行工具将这套加解密流程融入CI/CD实现接口安全的自动化回归测试。最后一个非常重要的提醒用于测试的密钥和IV务必使用测试环境的配置严禁使用生产环境的真实密钥。建议将测试密钥通过Postman的环境变量来管理并且不要将其提交到版本控制系统中。对于团队协作可以利用Postman的团队工作区和环境变量同步功能安全地共享配置而不暴露密钥明文。