接口签名先防重放
接口签名经常被写成“把参数拼起来算个 hash”,上线后才发现真正危险的不是别人不会算签名,而是别人拿到一次合法请求后反复发送。对支付、发券、改权限这类接口来说,签名正确不代表请求应该再次生效。
这篇文章不打算把防重放签名讲成抽象原则,而是放到真实项目里拆:它受哪些机制影响,哪里需要提前定责,失败以后怎样恢复。我的取舍是,先让状态和责任可解释,再追求漂亮的封装或更快的路径。
签名只能证明没被改
签名通过说明参数和密钥匹配,不说明请求是第一次出现。防篡改和防重放是两件事,需要不同规则。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
在机制上,timestamp、nonce、签名串、服务端缓存、幂等键、时间窗口会一起影响防重放签名。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:把签名校验、时间窗口和 nonce 校验拆成三个步骤。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:只校验签名值,会放过复制粘贴的旧请求。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
我会用一个朴素标准判断这一段是否完成:同一签名请求再次发送时会被拒绝,合法新请求不会被误杀。如果团队回答这个问题时还要靠猜,说明防重放签名仍然停留在口头规则,没有真正进入实现和验收。
这张图只画和“签名只能证明没被改”直接相关的路径,重点是让边界、状态和失败出口都能被看见。
时间窗口要短但可用
timestamp 能限制旧请求,但窗口太短会误伤弱网,太长又给重放留下空间。窗口要结合业务风险和客户端时钟偏差。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
放到“时间窗口要短但可用”这一节,这些机制不是背景板,timestamp、nonce、签名串、服务端缓存、幂等键、时间窗口会一起影响防重放签名。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:普通接口可以几分钟,高风险接口要更短并配合 nonce。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:完全相信客户端时间,会遇到时钟不准的问题。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
对“时间窗口要短但可用”来说,验收标准可以更具体一点:同一签名请求再次发送时会被拒绝,合法新请求不会被误杀。如果团队回答这个问题时还要靠猜,说明防重放签名仍然停留在口头规则,没有真正进入实现和验收。
针对“时间窗口要短但可用”,可以把检查清单压成三项:先确认对象是谁,再确认它的生命周期在哪里结束,最后确认失败以后谁负责接手。清单越短,越能逼出真正关键的规则。
nonce必须服务端记账
nonce 的意义在于同一窗口内只能用一次。如果服务端不存,它就只是一个随机字符串。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
放到“nonce必须服务端记账”这一节,这些机制不是背景板,timestamp、nonce、签名串、服务端缓存、幂等键、时间窗口会一起影响防重放签名。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:把 nonce 或 requestId 写入 Redis,并设置过期时间。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:只要求客户端传 nonce,不查重复,等于没有防重放。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
对“nonce必须服务端记账”来说,验收标准可以更具体一点:同一签名请求再次发送时会被拒绝,合法新请求不会被误杀。如果团队回答这个问题时还要靠猜,说明防重放签名仍然停留在口头规则,没有真正进入实现和验收。
这张图只画和“nonce必须服务端记账”直接相关的路径,重点是让边界、状态和失败出口都能被看见。
下面这段只作为边界表达示例,不建议脱离业务直接复制:
- sign = hash_hmac("sha256", canonical_query, secret)
- key = "nonce:" . app_id . ":" . nonce
签名串要稳定可解释
参数排序、空值处理、编码方式都要固定,否则不同客户端会算出不同结果。签名规则要能被测试样例验证。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
放到“签名串要稳定可解释”这一节,这些机制不是背景板,timestamp、nonce、签名串、服务端缓存、幂等键、时间窗口会一起影响防重放签名。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:提供一组标准参数和期望签名,给客户端联调。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:临时拼字符串最容易在数组和中文编码上出问题。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
对“签名串要稳定可解释”来说,验收标准可以更具体一点:同一签名请求再次发送时会被拒绝,合法新请求不会被误杀。如果团队回答这个问题时还要靠猜,说明防重放签名仍然停留在口头规则,没有真正进入实现和验收。
针对“签名串要稳定可解释”,可以把检查清单压成三项:先确认对象是谁,再确认它的生命周期在哪里结束,最后确认失败以后谁负责接手。清单越短,越能逼出真正关键的规则。
这张图只画和“签名串要稳定可解释”直接相关的路径,重点是让边界、状态和失败出口都能被看见。
幂等记录保护业务结果
防重放拦请求,幂等保护业务结果。即使请求绕过重放窗口,业务层也应该识别同一订单或同一操作。 这一步的价值不是让流程变得复杂,而是把隐含假设摊到桌面上。只要假设能被看见,开发、测试和产品就能围绕同一个边界讨论,而不是在问题出现后各自补解释。
放到“幂等记录保护业务结果”这一节,这些机制不是背景板,timestamp、nonce、签名串、服务端缓存、幂等键、时间窗口会一起影响防重放签名。它们不是文档里的并列名词,而是会在一次真实操作里互相拖拽:一个环节慢了、断了、被重试了,后面的状态就会跟着变化。
落地时可以先做一个小动作:高风险操作使用业务 id 做幂等键。 这件事不一定需要大改架构,但它能让问题发生时留下足够线索。很多难查的问题,缺的不是技术能力,而是最开始没有留下可追的痕迹。
这里的反面例子也要写清:只在网关层防重放,业务层仍可能重复扣减。 它通常不会在演示环境暴露,因为演示路径短、数据少、角色单一;一旦进入真实用户和长期运行,问题就会变得很难复盘。
对“幂等记录保护业务结果”来说,验收标准可以更具体一点:同一签名请求再次发送时会被拒绝,合法新请求不会被误杀。如果团队回答这个问题时还要靠猜,说明防重放签名仍然停留在口头规则,没有真正进入实现和验收。
高风险接口还要看业务幂等
签名和 nonce 能挡掉一部分重放,但高风险接口还要有业务幂等。比如发券、扣款、改权限,不应该只靠请求层判断是否重复,还要看业务对象是否已经处理过同一动作。
幂等键最好来自业务语义,而不是随机生成后就丢掉。订单号、操作批次、用户动作 id 都比一次性的请求序列更利于后续排查。
验收时可以拿同一个合法请求连续发送三次,再把 timestamp 改成窗口内的新值发送一次。前者应该被重放拦住,后者如果业务对象相同,也不应该重复生效。
签名失败也要能排查
签名失败不要只返回“验签失败”。对外可以保持简短,对内日志必须记录参与签名的参数摘要、排序结果、时间偏差和 nonce 状态。
这类日志要注意脱敏,不能把密钥或完整敏感参数打出来。好的日志不是暴露更多数据,而是让服务端能判断失败来自参数缺失、编码不一致、时间过期还是 nonce 重复。
接口签名先防重放的验收样本
最后还要补一组验收样本。样本不需要覆盖所有情况,但要覆盖正常、异常、边界和恢复。对“接口签名先防重放”来说,只有这四类样本都能解释,文章里的建议才不是停留在原则层面。
我会特别关注恢复样本:失败后状态是否可追,下一次是否能继续,重复执行是否安全。很多系统不是败在第一次失败,而是败在失败后的第二次处理。
最后用三个问题收住
第一,谁拥有防重放签名的最终解释权。没有 owner 的规则,短期靠人记,长期靠运气。
第二,失败以后系统会留下什么证据。证据不是越多越好,而是要能回答:发生在什么条件下、处理到哪一步、下一次应该从哪里恢复。
第三,这个方案适合哪些场景、不适合哪些场景。签名校验能证明请求未被篡改,但防重放还需要服务端记住已经处理过的请求。,所以不要把一次有效实践包装成万能答案。
如果这三个问题能说清,防重放签名就不再只是一个技术点,而会变成团队协作中的稳定边界。后续无论换框架、换人员还是换业务规模,都能沿着这条边界继续调整。
