基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
  • 知识卡片碎片时间过考点
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
    • AI 热点每日 AI 资讯与动向
AI 工作台
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-11-13 13:00

Webhook 防重放需补齐时间窗与幂等

dev.to · AI#接口安全#Webhook#幂等
Editor brief · 编辑速览

文章指出,仅验证签名无法阻止合法请求被重复提交,并给出签名校验、时间戳校验和请求去重的顺序管线。示例采用 HMAC-SHA256、前后 300 秒时间窗及基于 request_id 的重放检测。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

验过签的 Webhook,仍可能被重复执行。攻击者只要拿到一份合法请求及其签名,就可能原样重发,让同一笔业务再次修改状态。原文提出的防线是:先验签,再限制时间偏差,最后检查请求是否已经处理。

这套顺序容易理解,也适合作为服务端接收 Webhook 的入门框架。但示例离生产可用还有距离:内存缓存无法覆盖多实例,并发检查存在空隙,自写缓存还有递归缺陷。值得学的是三层校验各自负责什么,以及为什么「通过校验」还不等于「业务只执行一次」。

签名正确,为什么还会出问题

数字签名校验回答的是:这份请求是否符合约定密钥和签名算法,内容有没有被篡改。它没有回答另一个问题:这份请求以前是否已经执行过。

原文使用 HMAC-SHA256,根据密钥和原始请求体计算摘要,再用 hmac.compare_digest 比较签名。攻击者即使不知道密钥,只要能获取合法请求与对应签名,原样重发时仍能通过检查。

所以作者把接收流程拆成三关:

  1. 校验签名,失败对应 401 Unauthorized。
  2. 校验数据结构和时间戳,允许偏差为 ±300s,超出范围在流程图中对应 400 Bad Request。
  3. 检查 request_id 是否存在,重复请求对应 409 Conflict,首次出现则登记并返回成功。

这里的时间限制也不是完整答案。它能挡住过旧的请求,却挡不住有效窗口内的重复提交。请求刚发出几秒就被重放,仍然需要第三关拦截。

验签确认请求可信,时间窗口限制请求寿命,去重记录限制重复执行。

三者互相补位。还有一个隐含前提:时间戳和请求标识必须受到签名保护。示例把它们放在整个请求体里一起签名,攻击者不能随意改成新时间、新 ID,再沿用旧签名。

代码值得保留的是顺序,参数需要结合业务

示例用 Pydantic 定义 WebhookPayload:request_id 是非空字符串,timestamp 是整数,data 是字典。下面这段负责拒绝与服务器时间相差超过 300 秒的请求:

    @field_validator('timestamp')
    @classmethod
    def validate_timestamp(cls, v):
        # 允许的时间偏差为 5 分钟(300 秒)
        if abs(time.time() - v) > 300:
            raise ValueError("Timestamp drift too large")
        return v

关键是 abs(time.time() - v) > 300:过旧和过于超前的时间都会被拒绝。发送方与接收方时钟不准、请求排队太久,也可能触发它。300 秒是示例选择,原文没有提供这个参数适合哪些业务的测量依据。

完整处理方法把三关串了起来:

    def process_webhook(self, raw_body: bytes, signature: str):
        # 1. 签名验证
        if not self.verify_signature(raw_body, signature):
            raise UnauthorizedError("401 Unauthorized: Invalid Signature")

        # 2. 数据结构验证
        data = json_loads(raw_body)
        payload = WebhookPayload(**data)

        # 3. 防止重放攻击(幂等检查)
        if payload.request_id in processed_requests:
            raise ConflictError("409 Conflict: Replay detected")

        processed_requests[payload.request_id] = True
        return {"status": "success"}

要看清第一行验签使用的是 raw_body。如果接收端先解析 JSON,再重新序列化后验签,空格、字段顺序等字节变化可能让签名失配。接到 Node 服务时,也应按发送平台的签名协议保留所需的原始请求体。

去重缓存配置为 TTLCache(maxsize=10000, ttl=600),最多保存 10000 条记录,保留 600 秒,也就是 10 分钟。时间窗口允许请求最多提前 300 秒,因此一份首次被接收的请求,理论上还可能在随后约 600 秒内满足时间条件。这个组合有逻辑联系,但原文没有解释容量上限如何根据流量确定。

还要区分异常与 HTTP 响应。流程图列出了 200、400、401、409,代码却没有 Web 框架路由或异常处理器。自定义异常中的状态码文字不会自动变成 HTTP 状态码;Pydantic 校验失败如何映射响应,也需要接入层处理。

内存去重能演示流程,却撑不起严格幂等

原文明确承认:单进程内存缓存无法在多服务器之间共享。一次请求落到实例 A,重放请求落到实例 B,B 的缓存为空,就可能再次放行。AWS Lambda 这类部署同样存在实例状态隔离问题。作者因此提出,分布式生产环境需要 Redis 等共享持久层。

不过,只把字典换成 Redis 仍不够。示例中的「检查是否存在」与「写入记录」是两个动作。并发请求可能同时检查到不存在,然后分别继续执行。共享存储解决的是大家能否看到同一份记录,还需要原子操作解决谁能取得处理资格。

更大的空白在业务执行部分。代码登记 ID 后直接返回 {"status": "success"},没有展示任何实际状态修改。如果登记成功后业务失败,后续重试可能被拒绝;如果先执行业务再登记,中途崩溃又可能留下重复执行的机会。

这段代码演示的是接收端去重检查,还没有实现完整的业务幂等。

缓存容量也属于安全边界。达到 10000 条后,被提前淘汰的记录可能仍处于请求有效期内。原文未给容量压力测试,不能据此保证整个时间窗口都受到保护。

作者还为 cachetools 和 orjson 提供了标准库回退:分别使用 dict、time 和 json。这样可以减少部分依赖缺失导致的 ModuleNotFoundError,但 Pydantic 仍然直接导入,因此不能理解成完全不依赖第三方包。

自写 TTLCache 还有明确缺陷:访问过期键时,__getitem__ 调用 _remove;_remove 又用 if key in self 检查,重新进入 __contains__ 和 __getitem__,形成递归。过期清理可能因此失败。原文提到了自写缓存的维护成本,却没有识别这个具体问题。

我的判断

作者对威胁的判断是对的:只验签不足以防重放,接收端必须考虑请求的新鲜度与重复处理。把检查顺序明确写出来,也有助于团队评审接入代码。

但文章没有拿出足够的验证证据。所谓测试部分只展示了 uuid-123、{"val": 1} 和 super-secret-key 组成的正常路径,没有展示重复提交、时间超限、过期清理或并发请求的断言与结果。正文的三项工程考虑还各自重复了一遍。它更适合作为方案草稿,不能当成已验证的生产实现。

与维护自写回退缓存相比,普通团队通常更适合先把依赖安装、版本管理和构建检查做好;与单纯记录请求 ID 相比,有真实业务写入的系统还需要考虑数据库唯一约束、事务或处理状态。具体采用哪一种,取决于业务更新发生在哪里,原文没有给出比较数据。

文末另有 AI 协议宣传,宣称节省约 90.1% 的能耗和 token 开销,并列出 23.4x 等数字,却没有提供可核对的测量过程,也没有建立这些指标与 Webhook 防护效果的关系。这部分不能充当架构正确性的证据。

能拿来做什么

  • 给现有 Node Webhook 入口做一次流程检查,确认验签使用协议要求的原始字节,并且时间戳、请求标识都受到签名保护。
  • 把重复请求、超过 ±300s 的请求、缓存过期和并发到达加入验证场景,检查最终业务状态是否只更新一次。
  • 准备从单实例扩到多实例时,把去重状态迁到共享存储,并验证检查与登记能够原子完成,同时明确业务失败后的重试方式。
  • 让 AI 生成接入代码时,要求它同时交代异常到 HTTP 状态码的映射、依赖安装方式,以及登记请求后业务执行失败如何恢复,再据此审查代码。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
MCP 服务上架 Claude 前的自查要点
下一篇
用 Next.js 实测 A2UI 智能体界面协议

资讯速览

dev.to · AI · 2026-11-13 13:00

AI 评分 8.0 / 10

相关标签

#接口安全#Webhook#幂等

继续阅读

  • Spring Boot MCP 服务的鉴权与支付防护
  • MCP 服务上架 Claude 前的自查要点
  • 用 Next.js 实测 A2UI 智能体界面协议
  • Gemini 新模型与混合编排降低编程成本
  • 智能体选型:自建循环还是采用框架