news 2026/10/13 11:05:27

Postman脚本加密JSON报文:动态参数与签名避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Postman脚本加密JSON报文:动态参数与签名避坑指南

我记得年前联调一个对公接口时,同事在本地脚本里跑得好好的,挪到 Postman 里就报验签失败。一开始怀疑算法写错了,后来一行行对比才发现:报文体里有三个动态字段,脚本每次生成的顺序和取值时机不同,导致参与签名的明文和实际发送出去的密文根本不是同一份数据。

Postman 的Pre-request Script和Tests是两段独立的沙箱脚本,很多人把它们当成一个普通 Node 环境来写,结果漏掉了变量传递、执行顺序、加密编码这些细节。这篇就把"Postman 脚本加密 JSON 报文 + 动态参数"这件事从头到尾拆开:动态参数怎么生成、怎么塞进报文体、怎么和签名/加密链路配合,以及跑通之后最容易翻车的几个坑。适合给网关接口做过联调、正在写接口自动化用例的测试同学,也适合后端同学作为代码级参考。

1. 先别写脚本:理清动态参数与加密边界

很多人一上来就写CryptoJS.AES.encrypt,写到一半才发现:报文体里有时间戳、随机数、业务流水号,这些字段每次都变,那到底"先加密再放参数"还是"放完参数再加密"?顺序反了,服务端一解密就散架。

1.1 报文体里的"动态"到底指什么

所谓动态参数,本质上是每次请求都不相同、且参与业务语义的字段。我习惯归成三类:

类型示例特点是否参与加密
时间类timestamp、reqTime秒级或毫秒级,随请求时刻变化通常参与
随机类nonce、random每次请求随机生成,防重放通常参与
业务类bizNo、orderId、serialNo业务唯一键,本次请求生成参与

这里有个很容易被忽略的点:appId、version这类固定字段属于静态参数,也要参与加密和签名,但它们不属于"动态生成"的范畴。动态参数的难点不在于加密本身,而在于你要保证:脚本生成的"这一份"参数值,被同时用于三条链路——明文拼装、签名计算、最终 JSON 发送。三个地方用到的值有任何不一致,服务端验签就必然失败。

1.2 为什么"整个报文加密"会踩更多坑

一种常见的做法是把整个 JSON body 全部加密成一段密文,再塞进外层data字段。这样确实简单,但会带来两个问题。

第一,外层必留字段。就算业务报文全加密,网关层仍然要解析appId、timestamp、sign这些字段来做路由、鉴权和验签,所以你不能"全部加密"。最终请求体往往是:

{ "appId": "your-app-id", "timestamp": "1735689600", "nonce": "a1b2c3d4", "data": "U2FsdGVkX1+sQqHj...", "sign": "9f8d7a..." }

第二,验签的粒度不清晰。加密后生成的密文是动态的,服务端怎么验证"这条密文是不是被篡改过"?通常的做法是:先把原始明文做一次摘要(比如 SHA256),把摘要放进明文一起加密;签名则覆盖外层可见字段 + 密文摘要。这样动态参数既进了密文,又通过摘要牵制住了篡改行为。

所以搞清楚"哪些字段参与摘要、哪些字段参与签名、哪些字段只做传输",比急着写加密代码重要得多。

2. 选型决策:SIGN、AES-CBC 还是 HMAC,网关到底想防什么

"加密报文"这四个字看着具体,实际切开是三种完全不同的需求。我在项目里见过太多人把加密和签名混为一谈,最后联调时被服务端以各种姿势拒绝。

2.1 加密、摘要、签名三件事别混在一起

  • 加密(AES/RSA):目标是保密。让别人读不懂报文,用的是对称密钥或公钥。
  • 摘要(SHA256 等):目标是完整性。给一段明文算指纹,防止内容被改。
  • 签名(HMAC/RSA-Sign):目标是认证和防篡改。确认"发的人确实持有约定密钥"。

Postman 脚本里这三件事往往会同时出现。比如一个典型的网关请求改造链路是:对timestamp + nonce + 明文摘要做 HMAC-SHA256 生成sign,再用 AES 加密报文。服务端先验签,再解密,再校验摘要。每一步的产物不同,千万别图省事用一个函数输出代替另一步的输入。

2.2 加密粒度选型:字段级签名与整体密文

我在实际项目里遇到的接口大致有两种风格。

第一种,外层只做签名,报文明文传输。适用于对保密性要求不高的内部系统,重点是防篡改、防重放。此时动态参数按规则拼进签名串即可,脚本简单,调试也直观。

第二种,报文整体加密 + 外层签名。适用于报文里包含敏感业务数据(卡号、手机号、金额)的接口。此时脚本里需要两步:先加密得到data,再对data以及timestamp、nonce生成sign。一定注意:签名覆盖的是密文,不是明文。我见过有人把sign对明文生成,结果服务端一验签就发现签名和收到的密文对不上——因为明文在传输中根本不可见。

2.3 参数细节:CBC/ECB 模式与密钥长度不是随便填的

无论网关文档怎么写,出现 AES 几乎必然要求某种模式。我的默认选择是AES-CBC + PKCS7Padding + 随机 IV,这也是大多数服务端默认实现。

用 Postman 里的 CryptoJS 时,几个参数的真实含义值得说清楚:

CryptoJS 参数含义容易踩的坑
mode: CryptoJS.mode.CBC分组模式和 ECB 混用会导致各分块独立,密文可被重排
padding: CryptoJS.pad.Pkcs7填充方式后端若用 NoPadding 恰好在块整数倍时会解密失败
iv初始向量固定 IV 会被重放攻击,建议每次动态生成,同时放进报文
key对称密钥必须严格 16/24/32 字节,长度不对后端直接报错

关于密钥长度,这里有个很隐蔽的细节:如果你传给 CryptoJS 的 key 是字符串,CryptoJS 会把它当作 passphrase,走 OpenSSL 的 KDF 派生真实密钥;而大多数服务端语言(Java/Golang)会直接拿原始字节做密钥。两边算法表面上一致,实际根本对不上。解决办法是用CryptoJS.enc.Utf8.parse()强制转成 WordArray,后面模板部分会写清楚。

3. Pre-request Script 的核心动作:生成动态参数并把它们缝进请求体

Postman 的脚本执行顺序是:Pre-request Script先跑 → 发送请求 →Tests再跑。动态参数必须在 Pre-request 阶段生成,并在这个阶段完成变量写入、报文拼装、签名加密全套动作。

3.1 为什么用环境变量做"桥接",而不是直接改 Body 再发送

很多人会尝试在脚本里直接pm.request.body.raw = "..."。这个 API 在 Postman 里确实能改请求体,但它有个问题:改了之后,Postman 的 UI 上不会同步显示,且一旦脚本报错,请求体还是旧的,排查起来极其迷惑。

我强烈推荐的做法是:在请求 Body 的 raw JSON 里写一个占位符{{requestBody}},脚本里用pm.environment.set('requestBody', 最终JSON)来喂数据。这样发送前 Postman 会自动把占位符替换成环境变量里的值,逻辑清晰,调试时把环境变量的值打印出来也一目了然。

3.2 动态参数注入的两种写法:模板替换与整段重设

如果你的报文体里只有少量动态字段,可以用模板替换:

const rawBody = pm.environment.get('templateBody') || '{}'; const finalBody = rawBody .replace('{{timestamp}}', timestamp) .replace('{{nonce}}', nonce) .replace('{{bizNo}}', bizNo); pm.environment.set('requestBody', finalBody);

如果报文体本身就是脚本拼装的结果,那么直接构造对象即可,不需要模板替换。我建议对报文复杂的接口一律走"整段重设",因为字段一多,模板替换容易漏换某个{{var}},最终发出去的是带占位符的坏 JSON。

3.3 生成时间戳、nonce 与业务流水号的标准模板

动态参数生成不是随便写个Date.now()就完事,要考虑服务端的校验窗口和格式偏好。我现在的标准写法:

// 秒级时间戳,避免毫秒数导致签名不一致 const timestamp = Math.floor(Date.now() / 1000).toString(); // 16 字节随机数,转成十六进制字符串 const nonce = CryptoJS.lib.WordArray.random(8).toString(); // 业务流水号:时间戳 + 随机段,保证全局唯一 const bizNo = 'T' + timestamp + CryptoJS.lib.WordArray.random(4).toString();

注意时间戳位数。如果服务端接口文档写的是 13 位毫秒时间戳,那你签名串里就必须用 13 位,不能混用。我在排查线上问题时就遇到过:Pre-request 里生成的是 10 位秒级,请求体里手动填了 13 位毫秒级,两个值不一致,服务端认为时间戳被篡改。

4. 可照抄的完整模板:AES-CBC + SHA256 摘要 + HMAC-SHA256 签名

下面是一套我实际在用、可以直接复制进 Pre-request Script 的完整脚本。整个流程分六个动作:生成参数、拼装明文、计算摘要、AES 加密、HMAC 签名、注入环境变量。为了贴合常见网关场景,我把"明文摘要 + 密文签名"的混合方案写进去,这是多数安全要求中等偏上的服务端接口采用的策略。

4.1 完整 Pre-request Script

// ========== 1. 生成动态参数 ========== const timestamp = Math.floor(Date.now() / 1000).toString(); const nonce = CryptoJS.lib.WordArray.random(8).toString(); const bizNo = 'B' + timestamp + CryptoJS.lib.WordArray.random(4).toString(); // ========== 2. 组装业务明文 ========== const appId = pm.environment.get('APP_ID'); const rawPayload = { appId: appId, bizNo: bizNo, amount: '100.00', timestamp: timestamp, nonce: nonce }; // ========== 3. 对规范化明文计算 SHA256 摘要 ========== // key 按字典序排列,拼成 k=v&k=v 形式,保证服务端用同一规则重算 const canonicalStr = Object.keys(rawPayload) .sort() .map(k => `${k}=${rawPayload[k]}`) .join('&'); const digest = CryptoJS.SHA256(canonicalStr).toString(CryptoJS.enc.Hex); // 摘要放进明文,一起加密 const payloadToEncrypt = Object.assign({}, rawPayload, { digest: digest }); // ========== 4. AES-CBC 加密 ========== const aesKey = CryptoJS.enc.Utf8.parse(pm.environment.get('AES_KEY')); const aesIv = CryptoJS.enc.Utf8.parse(pm.environment.get('AES_IV')); const ciphertext = CryptoJS.AES.encrypt( JSON.stringify(payloadToEncrypt), aesKey, { iv: aesIv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ).toString(); // ========== 5. HMAC-SHA256 签名 ========== // 这里签名覆盖:请求方法+路径+动态参数+密文摘要(注意是 ciphertext 的 digest) const cipherDigest = CryptoJS.SHA256(ciphertext).toString(CryptoJS.enc.Hex); const signStr = [ 'POST', pm.environment.get('API_PATH'), timestamp, nonce, cipherDigest ].join('\n'); const signature = CryptoJS.HmacSHA256( signStr, pm.environment.get('SIGN_KEY') ).toString(CryptoJS.enc.Hex); // ========== 6. 注入请求体 ========== const finalBody = { appId: appId, timestamp: timestamp, nonce: nonce, bizNo: bizNo, // 业务号外层可见,便于服务端幂等去重 data: ciphertext, sign: signature }; pm.environment.set('requestBody', JSON.stringify(finalBody)); // 方便调试时在控制台确认 console.log('canonicalStr:', canonicalStr); console.log('finalBody:', finalBody);

这套脚本有几个值得说明的细节。

签名串的拼接顺序不是随便定的。我见过不同网关有截然不同的拼法:有的要求key=value&key=value,有的要求每个字段换行,还有的直接对密文做取摘要后再签名。上面写的join('\n')是常见的格式,但也必须替换成你服务端文档约定的格式。顺序影响结果,别和我一样想当然。

摘要和签名虽然都是 SHA256,但用途不同。digest保证明文在传输中没被改动;signature保证外层身份和防重放。两者一个进密文,一个放外层,缺一个,安全模型就不完整了。

4.2 请求 Body 的写法与 Tests 里的校验

请求 Body 的 raw 区,只要写一个变量占位符就够了:

{{requestBody}}

Tests 里建议至少做三层校验:状态码、JSON 结构、业务码。如果响应体也是密文的,可以顺手在 Tests 里解一次密,直接确认服务端返回逻辑正确。

const resp = pm.response.json(); pm.test('HTTP 200', () => { pm.expect(pm.response.code).to.eql(200); }); pm.test('响应体包含 code', () => { pm.expect(resp).to.have.property('code'); }); // 如果响应体也有密文 data 字段,可以解密后断言业务字段 if (resp && resp.data) { const decrypted = CryptoJS.AES.decrypt( resp.data, CryptoJS.enc.Utf8.parse(pm.environment.get('AES_KEY')), { iv: CryptoJS.enc.Utf8.parse(pm.environment.get('AES_IV')), mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ).toString(CryptoJS.enc.Utf8); pm.test('响应解密成功', () => { pm.expect(decrypted).to.include('"success"'); }); }

4.3 一个完整的联调场景验证

我拿这套模板配过某网关的项目物料接口,流程大概是:

  1. 声明环境变量:APP_ID、AES_KEY、AES_IV、SIGN_KEY、API_PATH、BASE_URL。
  2. 在请求 Body 写{{requestBody}}。
  3. 把 Pre-request 脚本贴进去,在 Tests 里做解码校验。
  4. 第一轮就遇到了签名失败,原因是服务端要求的签名串是a=xx&b=yy排序后的结果,我代码里用了换行拼接。改成&拼接后,一次通过。

这说明一件事:算法本身在 Postman 里跑通并不难,难的是对齐服务端的约定。所以模板只解决了"在 Postman 里怎么构造加密报文"的问题,最终签名串格式、加密内容、时间戳精度,都必须以服务端文档为准。

5. 复盘常见坑:这五个问题基本占我联调排错的八成

脚本写完、请求能通,只是开始。我后来把遇到的问题汇总了一下,发现几乎都出在下面五个方面,任何一个都可能让你白白浪费一整个下午。

5.1 执行顺序与变量作用域:为什么 Tests 能看到值,Pre-request 却读不到

Postman 的脚本执行顺序是确定的:Pre-request 先跑,然后发送请求,最后跑 Tests。在 Tests 里设置的环境变量,在本次请求的 Pre-request 中是读不到的,只能在下次请求里读到。如果你某个接口的第二个请求依赖第一个请求响应的token,必须确认把"读取 token"的逻辑写在第二个请求的 Pre-request 里,而不是试图在第一个请求的 Tests 里直接发起第二次请求。

另外,Collection Runner 跑批时,每次迭代都会重新执行 Pre-request,环境变量会被重新覆盖。如果你在 Tests 里把它们改成了别的东西,下一次迭代的脚本很可能读到脏值。建议每条用例跑完把中间变量清理掉,防止串数据。

5.2 CryptoJS 编码:Key 到底该用字符串还是 WordArray

这是 CryptoJS 里最容易踩、但报错信息又最不明显的问题。直接写法:

// 错误写法:key 是普通字符串,CryptoJS 会走 KDF 派生 CryptoJS.AES.encrypt('some text', '1234567890abcdef');

这种写法在本地自测时可能可以加解密,但服务端在 Java 里拿到同一把密钥直接分组解密,结果必然是一堆乱码或解密异常。正确写法是:

// 正确写法:先 parse 成 WordArray const key = CryptoJS.enc.Utf8.parse('1234567890abcdef'); const iv = CryptoJS.enc.Utf8.parse('1234567890abcdef');

还有一点,摘要结果也是一样。CryptoJS.SHA256(str).toString()默认是十六进制字符串,如果你服务端期望的是 Base64,就必须写.toString(CryptoJS.enc.Base64)。两边的编码习惯不一致,也会产生"测了一个下午都验签失败"的情况。

5.3 时间戳窗口:签名通过但服务端返回"请求过期"

这类问题的隐蔽点在于:签名没问题,但服务端对时间有校验窗口(常见 5 分钟或 10 分钟)。一旦 Pre-request 生成的timestamp和请求体里实际发送的timestamp不是同一个值,服务端拿到的时间戳要么和签名对不上,要么已经超窗。

我排查过的一个实际案例是:脚本里生成的是秒级时间戳,但环境变量里残留了一个旧的 13 位毫秒时间戳。Pre-request 没有覆盖旧值,签名串用的又不是同一个来源,导致服务端验完时间后直接拒绝。建议在脚本开头先pm.environment.unset('timestamp'),确保每次都重新生成,不残留上一轮的值。

5.4 Base64 里的+号与 URL 编码问题

CryptoJS 加密输出是标准 Base64,里面可能出现+、/、=。如果接口要求把密文放在 query string 或表单里传输,+会被解析成空格,服务端解码时 Base64 直接错位。

解决方式有两种:

// 方式一:传给请求体之前先 URL 编码 const encodedCipher = encodeURIComponent(ciphertext); // 方式二:直接生成 Base64URL 变体 const b64url = ciphertext .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '');

选第二种方式的话,要确保服务端也支持 Base64URL 解码,否则两边格式不一致。另外,Postman 的 raw JSON 请求体走的是 JSON 传输,用+通常没问题;但如果你把它塞进pm.request.headers.add或动态拼进 URL,就要特别小心。

5.5 密钥管理:为什么我不建议把密钥直接写死在脚本里

脚本里硬编码密钥的坏处不用多说,一旦 Collection 分享给团队,等于把加密体系透了个底。Postman 的环境变量支持标记为 Secret,设置后变量的值在 UI 里被隐藏,但脚本中依然可以通过pm.environment.get()读取。

比较稳的习惯是:

  • 密钥放环境变量,而不是全局变量,避免跨环境串用;
  • 区分测试环境与生产环境的密钥变量,创建独立 Environment;
  • 分享 Collection 时不勾选"分享环境变量值"选项,只分享脚本结构;
  • 重要密钥不要写入console.log,我见过有人调试完忘了删日志,结果整个密钥被打进报告里。

如果团队有专门的密钥管理服务,也可以让 Pre-request 脚本通过某个内部接口动态拉取密钥,再放到当前环境变量中。不过这种方式会引入网络依赖和缓存策略,安全性确实更高,但也需要多写不少代码,可以根据实际需求取舍。


我现在的习惯是:任何涉及加密报文的接口,跑通第一遍之后,一定会在 Pre-request Script 末尾把关键中间值打印一遍,包括规范化明文、摘要、密文和签名串。不是为了炫耀,是真的方便排查——下次谁再说"验签失败",我只要看一眼日志就能判断是拼接规则问题、编码问题还是时间戳问题。这套模板你拿去改改,应该能少踩不少坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 11:04:57

从结果倒推设计产品方案:目标翻译、路径逆拆与最小闭环

做方案这件事,很多人有个惯性:拿到需求先顺着想。顺着用户场景想,顺着现有功能想,顺着资源限制想,最后排一版看起来哪儿都对的计划。但执行到一半往往才反应过来,整条路从一开始就没指向真正要的结果。我经…

作者头像 李华
网站建设 2026/10/13 11:03:24

PhotoImpact v13老照片修复与设计:百宝箱操作、对象管理与避坑指南

简介:Ulead PhotoImpact v13.0 是一款集图像制作、编修与网页设计于一体的图形处理工具,面向需要修图、制作网页横幅、合成图片素材的设计人员,也适合希望系统学习图像编辑的初学者。它内置方便快捷的克隆与编修工具,配合功能强大…

作者头像 李华
网站建设 2026/10/13 11:03:07

SSH证书签名实现临时免密登录:告别authorized_keys手动管理

不管你是不是刚入行,只要管过几台Linux服务器,一定经历过这种场景:有同事临时需要上一台生产机器看看日志,或者外部合作方要做一次紧急排障。传统做法是让他把公钥发过来,你手动塞到目标机器的 authorized_keys 里&a…

作者头像 李华
网站建设 2026/10/13 11:02:49

SQL递归CTE实战:组织层级分析与树形结构查询

刷 LeetCode SQL 题的时候碰到 3482 这道“分析组织层级”(Analyze Organization Hierarchy),起初我以为是普通的聚合统计,结果仔细一看,表里 manager_id 指向 employee_id ,典型到不行的自引用结构&am…

作者头像 李华
网站建设 2026/10/13 11:02:36

跨模态行人重识别CNN特征提取:从骨干网络到模态对齐的实战总结

1. 跨模态ReID与CNN特征提取的核心定位跨模态行人重识别(Cross-Modal Person Re-Identification)要解决的问题很具体:同一目标人物在可见光相机和红外相机下外观差异极大,算法需要判断两张不同模态的图像是否属于同一个人。这个任…

作者头像 李华