← 中文资源

简体中文版本

测试 Webhook 签名校验

按服务商的方式对请求体签名,断言篡改、重放与时间戳过期都会在任何业务逻辑之前被拒绝。

阅读英文原文

概述

Webhook 地址天生是公开的。如果 handler 直接信任请求体,任何知道地址的人都能把订单标记为已支付、取消订阅或触发退款。共享密钥加 HMAC 是最低要求:它证明请求来自知道密钥的一方,并把签名绑定到精确字节,修改金额就会导致校验失败。

步骤

  1. 对 ${timestamp}.${rawBody} 计算 HMAC,并校验时间窗口:
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  1. 用与服务商相同的方式生成签名,确认正常请求返回 200 且只写入一条事件。
  2. 签名后修改请求体,确认返回 400 且没有写入。
  3. 使用 10 分钟前的时间戳应被拒绝;同一事件 id 重复投递只处理一次。
  4. 验证必须使用原始字节:重新序列化 JSON 后空白变化,签名应失败。

验证

合法签名返回 200 并写入一条 evt_1;把 evt_1 改成 evt_2 后返回 400 且无记录;过期时间戳返回 400;同一事件投递两次仍只有一条记录。重新序列化后的相同 JSON 必须返回 400,说明校验针对原始字节。

注意事项

  • 先解析 JSON 再校验会导致签名永远失败或校验被跳过,必须在 JSON 中间件之前捕获原始请求体。
  • 比较必须使用 timingSafeEqual 并先判断长度,避免时序侧信道。
  • 密钥轮换期间请求头可能包含多个 v1,任意一个匹配即可,但每个都要常量时间比较。
  • 签名只证明发送方,不证明事件语义正确,业务校验仍需保留。
  • 时间窗口在时钟偏差与重放风险之间权衡,双方都要启用 NTP 并监控漂移。
  • 去重需要唯一约束,并在副作用之前写入事件 id。