公开的接收 URL 可以收到任何人的请求。攻击者可以向该 URL 发送伪造事件,因此请求必须经过认证后才能触发后续处理。
Bird 使用 Standard Webhooks 签名方案。它将事件标识符和尝试时间与请求体一起签名,因此更改其中任何一项都会使签名失效。
Bird 签名的内容是什么?
Bird 对事件标识符、投递尝试时间戳和原始请求体用英文句号连接后进行签名。
在验证签名之前,保持请求体不变。解析并重新序列化 JSON 可能改变 Bird 签名时使用的字节。
| 请求头 | 携带的内容 |
|---|---|
webhook-id | 事件标识符,在重试和重放中保持不变。 |
webhook-timestamp | 尝试时间,以秒为单位的 Unix 时间戳。 |
webhook-signature | 一个或多个签名,以空格分隔。每个签名以 v1, 开头。 |
在与以毫秒为单位的时钟进行比较之前,先将时间戳从秒转换为相应单位。
移除端点密钥的 whsec_ 前缀,然后对剩余部分进行 base64 解码以获取密钥字节。
用英文句号将标识符、时间戳和未修改的请求体连接起来。使用解码后的密钥对该字符串计算 HMAC-SHA256。使用恒定时间比较将计算结果与每个提供的签名进行比对,其运行时间不会泄露哪些字节匹配。
为什么我的签名始终不匹配?
错误的密钥或被修改的请求体会导致所有签名校验失败。
Web 框架通常会在你的处理程序运行之前解析 JSON。重新序列化该对象可能改变空白字符、键的顺序或数字格式。生成的 JSON 语义相同,但会产生不同的签名。
将该路由配置为保留原始请求体。检查密钥是否属于该端点,尤其是在部署或轮换之后。
我的处理程序应该拒绝什么?
当没有签名匹配或已签名的时间戳超出允许的时间窗口时,拒绝该请求。
尝试 webhook-signature 中的每一个签名。在密钥轮换期间,一次投递会携带来自多个有效密钥的签名。接受任何匹配的签名可让使用任一密钥的接收端继续正常工作。
在你的时钟两侧使用五分钟的时间戳容差。十分钟前被截获的请求即使签名未变也会校验失败。保持服务器时钟准确,以免拒绝合法的投递。
将 webhook-id 与已存储的事件进行比对。识别出的重复事件应返回成功而无需重复处理,因为重试同一次投递并不会产生新事件。
如果我拒绝了一次投递会怎样?
Bird 会对收到错误响应或在超时前未收到响应的投递进行重试。
例如,400 响应会记录拒绝并使该投递保持可重试状态。所有非 2xx 响应都遵循重试策略。该状态码有助于你在日志中诊断失败原因。
重试计划在调整前大约跨越 27.5 小时,给你留出修复错误密钥的时间。失败的 webhook 重试描述了重试计划以及如何在之后重放遗漏的事件。
仅在验证并安全存储事件后,或识别出已存储的重复事件后,才返回 2xx。Bird 在重放期间会跳过已成功投递的事件,因此确认未验证的请求将阻止通过该机制进行恢复。
我必须自己实现验证吗?
当你在 Bird SDK 中使用 webhooks.unwrap 时,无需自己实现验证。将原始请求体和请求头传递给它即可。
该辅助工具会在返回解码后的事件之前检查签名和时间戳。你的应用仍然需要通过 webhook-id 进行去重,因为已完成工作的记录由你的应用维护。
兼容的 Standard Webhooks 验证库可以执行相同的检查。webhooks 指南包含示例和手动实现方式。
简而言之
验证原始字节。
解析并重新序列化 JSON 可能改变 Bird 签名时使用的字节。请保留原始请求体用于验证。
同时检查时间戳和签名。
五分钟的时间戳容差可限制被截获请求的重放利用。另外通过 webhook-id 对已存储的事件进行去重。
尝试每一个提供的签名。
密钥轮换会产生重叠的签名。只要匹配任意一个有效签名,部署即可继续。
仅确认已验证且已存储的事件。
Bird 会对非 2xx 响应进行重试,并在重放期间跳过已成功投递的事件。对于已存储的重复事件,直接返回成功而无需重复处理。