Postman调试API签名与时间戳:从报错到自动生成签名脚本
最近在调一个开放平台的接口我打开Postman填好URL和Body点Send几秒钟后响应体里孤零零挂着一行字{msg: 请求缺失签名Sign}我以为是漏了哪个参数补上签名后又来一个{msg: 请求缺失时间戳}那种感觉就像拆盲盒每修一个错又蹦出下一个。后来静下心把签名机制捋了一遍才明白这两个报错背后其实是同一件事你这个请求压根没有通过服务端的“鉴权门禁”。这其实是很多对外开放API的安全常规操作Postman作为最常用的接口调试工具能不能把签名和时间戳这类动态参数处理好直接影响联调效率。这篇文章就讲讲我在这类报错上的完整排查思路和实操方案既照顾第一次对接签名接口的新手也适合老手查漏补缺。1. 这两个报错到底在说什么签名与时间戳机制拆解1.1 为什么接口要校验签名和时间戳在公网上部署的接口只要知道URL任何人都能发起请求。如果接口不做任何校验别人可以伪造你的身份调用下单、查询、修改数据甚至把请求里的金额改掉再发给服务端。为了解决这个问题大部分平台会采用“身份标识 签名 时间戳”的组合校验。AppKey 相当于客户端的身份证号告诉服务端“我是哪个应用在调用”AppSecret 是这个身份证对应的密码不会直接出现在请求里而是参与签名计算。客户端把请求参数、时间戳、随机串、AppSecret 拼成一个字符串做摘要算法得到 Sign。服务端收到后用同样的规则重算一遍如果结果一致就说明这段请求内容确实是持有 AppSecret 的人发出的并且参数没被中途篡改。时间戳解决的是另一个问题防重放。即使签名没问题如果攻击者把一次合法的请求原封不动地再发一遍服务端如果不做时效校验就可能产生重复下单、重复扣款。所以服务端会要求请求带有客户端当前时间并只接受一定时间窗口内的请求比如前后5分钟。超出这个窗口直接拒绝。这个机制很像快递柜的取件码取件码本身是动态的过期就失效别人捡到一段旧请求也做不了什么。所以“请求缺失签名Sign”和“请求缺失时间戳”这两个响应体提示并不是什么深层故障而是服务端在入口处发现请求包不满足最基本的校验条件。前者说明没有带 Sign后者说明没有带 timestamp或者字段名不叫 timestamp导致服务端没认出来。1.2 常见的签名算法套路与请求结构不同平台的签名规则千差万别但万变不离其宗。一个典型的带签名请求通常包含这么几部分。字段含义示例appKey应用标识服务端分配8f2e0c1a9d4btimestamp客户端时间戳秒级或毫秒级1730000000 或 1730000000123nonce随机字符串防止重放可选6f2a9c81e0sign签名结果由其余参数和secret计算9e0b7d8f... (32或64位)签名算法虽然叫“算法”但它真正的核心不是加密而是“约定”。服务端和客户端约定好把哪些参数、按什么顺序拼接、是否加盐、用什么摘要函数。常见的做法是将除文件流外的请求参数按 key 的字典序升序排列用key1value1key2value2的方式拼接成待签名串在待签名串末尾拼接上约定的密钥比如key你的AppSecret对该字符串做 MD5 或 SHA256 摘要得到 sign。这些参数放在哪里也由接口文档决定。我见过放在请求头Headers里的也见过塞在 POST 表单或者 JSON Body 里的。Postman 调试时最容易踩的坑就是文档明明说放请求头你却在 Body 里加了个 sign结果服务端依旧提示“请求缺失签名Sign”。服务端的校验顺序不同平台也不太一样。有些先校验时间戳是否存在、是否过期再校验签名有些反过来。这也是为什么你补完签名后可能又会报“请求缺失时间戳”因为第一道校验刚过第二道又把你拦下了。我们在 Postman 里处理这类问题思路要跟着服务端的节奏走先保证字段都在再保证值都正确。2. 在Postman里手动补全签名和时间的正确姿势2.1 先看清接口文档要求接到“请求缺失签名Sign”这类报错之后我建议先别急着在 Postman 里胡乱加参数。第一件事是打开接口文档确认下面四件事。签名参数的位置是在请求头、URL查询参数还是请求体里具体字段名叫什么时间戳的单位秒级是10位数字毫秒级是13位数字。单位写错服务端通常不会再提示“缺失”而是会报“签名错误”或“时间戳过期”。签名算法和拼接规则是 MD5 还是 SHA256是拼接整个 Body 还是只拼接几个指定参数密钥拼在中间还是末尾参与签名的范围有些平台凡是请求里的参数都要参与签名有些只签业务参数有些连 header 里的自定义字段也要参与。这个决定你后续写脚本时待签名串怎么构造。有些平台的文档写得不清楚那就只能靠试。此时最直接的帮手是 Postman Console它可以打印出请求实际发出的 headers 和 body对照服务端返回的提示能很快定位是哪一步没对上。2.2 手动添加请求头或请求体参数假设文档要求签名参数放在请求头我们在 Postman 里的操作是这样。打开目标请求切到 Headers 标签页逐行添加appKey平台分配给你的应用标识timestamp当前时间的 Unix 秒级时间戳nonce一段随机字符串可以先随便写sign签名值先随便填一个等会再算如果文档要求参数放在 Body 里就看你的 Body 格式。form-data 或 x-www-form-urlencoded 就继续在键值区加行raw JSON 就在 JSON 结构里补字段。很多新手会把这里搞混明明文档写的 “请求头携带”结果为了图方便把参数塞在 Body 里服务端用固定的字段名去请求头里取自然是取不到然后继续返回“请求缺失签名Sign”。这里的核心原则是服务端说在哪里取就放在哪里字段名的大小写也要严格一致。timestamp和Timestamp在一些服务端会被当成两个不同字段。2.3 手工算一次签名验证整条链路手动算签名这一步的意义不是让你以后每次都手算而是为了先排除“缺失”类问题。因为只要 Sign 的值不对服务端的提示大概率会变成“签名错误”而不是“缺失签名”。看到报错变化说明请求终于被服务端成功解析到了签名和时间戳字段。我举一个最简单的签名规则例子很多内部系统的签名逻辑都长这样。参与签名的参数有appKeytest-app-123 timestamp1730000000 nonceabc123先把参数按 key 字典序升序排列得到appKeytest-app-123nonceabc123timestamp1730000000然后在这串内容最后拼接密钥。假设 AppSecret 是my-secret-key那么待签名字符串为appKeytest-app-123nonceabc123timestamp1730000000keymy-secret-key再对这个字符串做 MD532位小写得到 sign。在命令行可以用echo -n appKeytest-app-123nonceabc123timestamp1730000000keymy-secret-key | md5sum把输出结果填到请求头的sign字段再点 Send。如果接口其他逻辑正常要么直接返回业务数据要么报“签名错误”。如果是后者就说明时间戳和 sign 字段确实被服务端收到了接下来只需要对着签名规则检查拼接顺序、参数范围、密钥是否一致。这里有个很典型的细节时间戳一定要用你发送请求那一刻的时间。我之前为了省事直接把昨天接口文档示例里的timestamp1730000000抄过来服务端先校验时间窗口一看超过五分钟就直接拒了。后来换成用date %s现场生成当前秒级时间戳签名才通过。3. 一劳永逸用Pre-request Script自动生成时间戳和签名3.1 Pre-request Script 是做什么的手动算签名的模式在只有一两个请求时还能接受。但一旦接口集合里有几十个接口或者签名参数里有动态业务字段比如订单金额、用户ID每次都要手工拼串和算摘要效率太低而且容易出错。Postman 的 Pre-request Script 就是为这种场景准备的。Pre-request Script 是请求发送之前会执行的 JavaScript 脚本。我们可以在里面生成随机字符串、取当前时间、计算签名然后通过变量或请求头操作把它注入到即将发送的请求里。与之对应的是 Tests 页签那是在响应返回之后执行的两者别搞混。脚本的作用域也很有用既可以在单个请求上写也可以在 Folder 上写还可以在 Collection 顶层写。执行顺序是 Collection 级脚本先跑再到 Folder 级最后才到 Request 级。所以那些面向整个接口集合统一的签名逻辑直接放在 Collection 级最合适。3.2 自动生成时间戳的几种写法Postman 的脚本运行在 Node.js 风格的沙箱里获取时间戳最常用的写法有这么几种。// 秒级时间戳10位 const timestampSecond Math.floor(Date.now() / 1000); // 毫秒级时间戳13位 const timestampMilli Date.now(); // ISO 8601 格式形如 2024-10-28T08:30:00.000Z const isoTime new Date().toISOString();用哪种完全取决于接口文档。假如文档写的是“timestamp 为 Unix 时间戳精确到秒”那就用秒级如果写的是“13位毫秒时间戳”就用Date.now()。我遇到过最坑的一次是平台文档写“时间戳”没有标注单位。我默认用了10位秒级结果服务端一直报“签名错误”。后来看了对方的技术支持给的示例请求里面 timestamp 是13位的 1730000000123 这种我改成毫秒后立刻通了。所以拿到文档后第一件事就是确认时间戳位数。3.3 自动生成签名的完整脚本示例下面是一个可复用的签名脚本按我前面提到的常见规则来写参数放请求头参与签名的字段是 appKey、timestamp、nonce使用 SHA256 摘要。你可以直接复制到 Postman 的 Pre-request Script 里把脚本里的 AppSecret 来源换成你自己的环境变量。// 从环境变量读取身份信息避免脚本里写死密钥 const appKey pm.environment.get(appKey) || 你的测试AppKey; const appSecret pm.environment.get(appSecret) || 你的测试AppSecret; // 生成秒级时间戳和随机字符串 const timestamp Math.floor(Date.now() / 1000).toString(); const nonce Math.random().toString(36).substring(2, 15); // 构造参与签名的参数对象 const params { appKey: appKey, timestamp: timestamp, nonce: nonce }; // 按 key 字典序升序排列并拼成 key1value1key2value2 形式 const keys Object.keys(params).sort(); const signStr keys.map(key key params[key]).join(); // 拼接密钥计算 SHA256 签名 const raw signStr key appSecret; const sign CryptoJS.SHA256(raw).toString(CryptoJS.enc.Hex); // 把参数写入请求头 pm.request.headers.upsert({ key: appKey, value: appKey }); pm.request.headers.upsert({ key: timestamp, value: timestamp }); pm.request.headers.upsert({ key: nonce, value: nonce }); pm.request.headers.upsert({ key: sign, value: sign }); // 打印调试日志方便对比服务端验签日志 console.log(待签名字符串:, raw); console.log(计算出的签名:, sign);这段脚本有几处需要重点说明。第一pm.request.headers.upsert是“有则覆盖无则新增”的语义。如果你用headers.add每次执行脚本可能追加多个同名 header导致服务端取到的值不稳定。我自己早期因此踩过坑后来统一用 upsert。第二CryptoJS是 Postman 沙箱内置的加密库不需要额外安装依赖。如果你用的平台要求 MD5 签名把最后一行换成const sign CryptoJS.MD5(raw).toString(CryptoJS.enc.Hex);第三Math.random().toString(36).substring(2, 15)生成的是一个看起来像随机串的 nonce。它不是密码学级别的随机数但对于防重放的常规校验已经够用。如果你对接的安全要求更高可以自己引入更可靠的随机数生成逻辑。如果签名参数不是放在请求头而是放在 JSON Body 里脚本稍微改一下。假设请求体是 raw JSON我们可以在生成签名后这样写入const body JSON.parse(pm.request.body.raw); body.appKey appKey; body.timestamp timestamp; body.nonce nonce; body.sign sign; pm.request.body.raw JSON.stringify(body);注意pm.request.body.raw只有在 raw 类型的 Body 下才有效。如果用的是 form-data 或 x-www-form-urlencoded建议直接切换到 raw JSON或者用脚本操作对应格式但操作起来不如 raw 方便。3.4 把脚本复用给整个接口集合单独在一个请求上贴脚本问题不大。但如果你有几十个接口都要签名一个个复制粘贴显然不是好方案。更好的做法是把签名脚本放到 Collection 级别的 Pre-request Script 里让集合下所有请求在发送前自动执行。具体操作是在 Postman 左侧选中你的接口集合点开集合菜单里的 Pre-request Script 页签把上面那段代码贴进去。这样集合里的每一个请求都会先跑这段脚本。当然前提是这些接口的签名规则一致且签名参数都放在相同的请求头位置。还需要把密钥放到环境变量里。Postman 右上角可以新建环境比如Test环境添加appKey和appSecret两个变量值填测试环境的凭据。脚本里通过pm.environment.get()读取。以后从测试环境切到生产环境只需要切换环境下拉框脚本不用动。这个习惯很重要否则一旦把生产环境的密钥写死在脚本里再同步给团队密钥就等于裸奔了。如果集合里有几个特殊接口不需要签名或者签名规则不同可以在 Collection 级脚本里做个判断。比如const url pm.request.url.toString(); if (url.includes(/api/public/)) { return; }把不需要签名的路径放进排除名单。这种写法在混合了开放接口和内部接口的集合里很实用不必为了个别例外而拆集合。4. 常见问题与排查技巧实录4.1 报错信息速查表我把日常对接中最常见的几类响应体提示和排查方向整理成了表格方便你遇到问题时直接对号入座。响应体提示可能原因排查方向请求缺失签名Sign没有携带 sign 字段sign 位置放错字段名不对按文档把 sign 加到指定位置检查大小写请求缺失时间戳没有携带 timestamp 字段用了别的字段名补充时间戳字段确认单位是秒还是毫秒签名错误签名算法不一致拼接顺序不同secret 不对参与签名的参数范围不对用 Console 打印实际请求比对待签名字符串时间戳过期时间戳不是当前时间单位错误本地时钟不准校准本地时间统一时间戳位数清除代理缓存nonce已使用同一个 nonce 被重复发送每次请求生成新 nonce重新执行脚本这张表只是兜底思路。真正卡住你的往往不是表格里“可能原因”这一列的常规项而是某些不容易察觉的细节。我在下面列几个最典型的坑。4.2 排查过程中的几个关键坑先说时间戳单位。服务端要求毫秒级你给的是秒级从字面上看字段有、也不“缺失”但服务端解析完发现时间在1970年附近于是要么报“时间戳过期”要么因为拿错误时间参与验签导致结果不一致最终报“签名错误”。排查时先看字段长度10位和13位一眼就能分辨。第二个坑是待签名串的拼接规则。我就见过一个平台要求先把参数做 URLEncode 再拼接签名 header 里的 key 也要参与计算。如果你只按普通keyvalue拼接两边永远对不上。遇到这种情况建议在脚本里把raw字符串用console.log打出来然后找一个服务端验签日志或者技术支持给的示例逐字符对比。第三个坑是字段大小写。Postman 的 Headers 默认会把 key 按你输入的原始形式发送不会自动统一大小写。有些服务端框架用RequestHeader(timestamp)取字段大小写不敏感而另一些服务端用签名校验中间件自己解析 header 时可能严格区分。所以文档写timestamp你就别填Timestamp写appKey就别填appkey。这是最廉价也最容易被忽略的一个错误。第四个坑是请求体格式。如果你用的是form-data但服务端期望的是raw JSON服务端解析 body 时可能直接拿不到业务参数参与签名的字符串自然对不上。遇到“签名错误”并且你确认拼接规则没问题时检查一下 Content-Type 和实际发送的 Body 结构。第五个坑和本地环境有关。如果本地机器时间跟真实时间差了很多时间戳即使生成正确也过不了服务端的时效校验。在 Linux 服务器上跑自动化任务时尤其容易遇到。排查时可以先对着手机时间校准系统时间再看是否还报“时间戳过期”。4.3 用Tests脚本自动检查响应体msg接口联调时响应体里的 msg 是判断问题最直接的入口。除了肉眼去看也可以写一个简单的 Tests 脚本让 Postman 自动断言“不应该出现缺失签名/时间戳”的提示。pm.test(响应体不应包含缺失签名或时间戳错误, function () { const resJson pm.response.json(); pm.expect(resJson.msg).to.not.include(请求缺失签名Sign); pm.expect(resJson.msg).to.not.include(请求缺失时间戳); });把这个脚本贴到请求的 Tests 页签里每次发送完后Postman 会自动把断言结果展示在响应区的 Test Results 标签下。当接口调通了断言是绿色通过当又出现“请求缺失签名Sign”断言会红色失败同时还会把具体信息打出来。有些平台返回的 msg 可能不是固定的中文而是错误码比如{code: 40001, message: invalid sign}。遇到这种就把断言里的字段改成对应的code或message正则匹配也可以。这个习惯不会改变请求行为但能在你调试一堆接口时快速发现“哪个请求又因为签名问题挂了”。4.4 我的一点实操心得与建议最后分享几条我自己的经验不一定写在哪个文档里但确实能提高排查效率。拿到一个带签名接口的报错时我一般分三步走。第一步先用最笨的方法手工填好 appKey、timestamp、nonce再去算一个固定签名发送一次。目标是让响应体里的“缺失”类报错消失。第二步把固定参数换成脚本自动生成用随机 nonce 和动态时间戳验证脚本逻辑是否正确。第三步再考虑是否把脚本提升到 Collection 级并接入环境变量。三步走比我一开始直接写脚本的效率高很多因为脚本一旦出错你很难分清是算法问题还是字段没到位。另外不要忽视 Postman Console。在 Postman 左下角打开 Console发送请求后能看到完整的请求头、请求体、响应体。很多时候我都是靠它发现“我以为我发了 sign实际上没发”或者“签名用的老环境变量”这类问题。它相当于浏览器的 F12 Network 面板是我排查接口联调问题时的第一工具。关于签名参数放请求头还是 Body我的建议是优先按平台文档来但在团队内部尽量统一。如果你维护的项目可以选择放在请求头更干净业务 Body 不用混入鉴权字段也方便做全局网关统一校验。当然这属于架构偏好具体还是要看服务端怎么设计。还有一个实用小技巧如果平台允许先申请一个测试用的 AppKey 和 AppSecret专门在 Postman 里调试。不要把生产凭据写进本地脚本更不要随手截图发给同事。Postman 的团队协作会把脚本同步给所有人一旦密钥泄露别人就能用你的身份调用接口后果很直接。我在多次折腾这类“请求缺失签名Sign”“请求缺失时间戳”的报错后最大的感受是这类问题其实不算难难的是静下心把请求从发出去到服务端接收的每一个环节都捋一遍。一开始我也觉得签名机制很麻烦直到把时间戳、nonce、签名串整条链路弄懂再看其他平台类似的签名接口基本半天之内就能把 Postman 调通。最后再分享一个小建议在你成功调通第一个带签名接口后一定把 Postman 集合导出留个备份最好把脚本里容易改的地方用注释标出来。别问我为什么等你下次重新搭环境时会感谢这个习惯。