最近在调一个开放平台的接口,我打开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 | 应用标识,服务端分配 | 8f2e0c1a9d4b |
| timestamp | 客户端时间戳,秒级或毫秒级 | 1730000000 或 1730000000123 |
| nonce | 随机字符串,防止重放,可选 | 6f2a9c81e0 |
| sign | 签名结果,由其余参数和secret计算 | 9e0b7d8f... (32或64位) |
签名算法虽然叫“算法”,但它真正的核心不是加密,而是“约定”。服务端和客户端约定好:把哪些参数、按什么顺序拼接、是否加盐、用什么摘要函数。常见的做法是:
- 将除文件流外的请求参数按 key 的字典序升序排列;
- 用
key1=value1&key2=value2的方式拼接成待签名串; - 在待签名串末尾拼接上约定的密钥,比如
&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 的值不对,服务端的提示大概率会变成“签名错误”而不是“缺失签名”。看到报错变化,说明请求终于被服务端成功解析到了签名和时间戳字段。
我举一个最简单的签名规则例子,很多内部系统的签名逻辑都长这样。参与签名的参数有:
appKey=test-app-123 timestamp=1730000000 nonce=abc123先把参数按 key 字典序升序排列,得到:
appKey=test-app-123&nonce=abc123×tamp=1730000000然后在这串内容最后拼接密钥。假设 AppSecret 是my-secret-key,那么待签名字符串为:
appKey=test-app-123&nonce=abc123×tamp=1730000000&key=my-secret-key再对这个字符串做 MD5(32位小写),得到 sign。在命令行可以用:
echo -n "appKey=test-app-123&nonce=abc123×tamp=1730000000&key=my-secret-key" | md5sum把输出结果填到请求头的sign字段,再点 Send。如果接口其他逻辑正常,要么直接返回业务数据,要么报“签名错误”。如果是后者,就说明时间戳和 sign 字段确实被服务端收到了,接下来只需要对着签名规则检查拼接顺序、参数范围、密钥是否一致。
这里有个很典型的细节:时间戳一定要用你发送请求那一刻的时间。我之前为了省事,直接把昨天接口文档示例里的timestamp=1730000000抄过来,服务端先校验时间窗口,一看超过五分钟就直接拒了。后来换成用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 字典序升序排列并拼成 key1=value1&key2=value2 形式 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 也要参与计算。如果你只按普通key=value拼接,两边永远对不上。遇到这种情况,建议在脚本里把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 集合导出留个备份,最好把脚本里容易改的地方用注释标出来。别问我为什么,等你下次重新搭环境时会感谢这个习惯。