Webhook 详解:签名负载与重试机制
你刚刚集成了一个第三方支付提供商。用户完成了购买,但你的数据库从未更新。为什么?因为本应通知你的应用的 Webhook 静默失败了。如果没有适当的签名验证和重试逻辑,Webhook 可能会成为神秘 bug 和安全漏洞的来源。
在本文中,我们将解释 Webhook 的工作原理,为什么必须验证签名负载,以及如何设计一个健壮的重试机制。我们还将向你展示如何有效地测试和调试 Webhook。
什么是 Webhook?
Webhook 是用户定义的 HTTP 回调。它们允许一个系统在事件发生时向另一个系统发送实时数据。例如,当支付处理完成后,支付提供商向您指定的 URL 发送一个 HTTP POST 请求,其中包含事件的详细信息。
与轮询(你的应用反复请求更新)不同,Webhook 在数据可用时立即推送给你。这使得它们高效且近乎实时。
安全问题:为什么要验证签名?
Webhook 端点是公开可访问的 URL。任何人都可以向它们发送 POST 请求。如果没有验证,攻击者可以伪造请求,例如,在没有实际付款的情况下将订单标记为已付款。为了防止这种情况,提供商使用密钥对每个负载进行签名。
HMAC 签名的工作原理
大多数提供商使用 HMAC(基于哈希的消息认证码)与 SHA-256。他们使用共享密钥计算请求体的哈希,并将其包含在像 X-Signature 这样的头部中。你的服务器重新计算哈希并将其与头部进行比较。如果它们匹配,则请求是真实的。
这是一个 Python 的简单示例:
import hmac
import hashlib
def verify_signature(payload, secret, signature):
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
始终使用像 hmac.compare_digest 这样的恒定时间比较函数来避免时序攻击。
要避免的常见错误
- 使用错误的密钥:每个环境(测试、生产)都应该有自己的密钥。
- 不验证时间戳:一些提供商包含时间戳以防止重放攻击。拒绝超过几分钟的请求。
- 记录密钥:永远不要在生产环境中记录密钥或完整签名。
设计可靠的重试
Webhook 通过互联网发送,因此失败是不可避免的。网络中断、服务器重启和端点超时。一个好的 Webhook 系统会使用指数退避重试失败的交付。
重试通常如何工作
提供商通常以递增的延迟重试:1 分钟后、5 分钟后、30 分钟后等,最多尝试一定次数(例如 5-10 次)。如果所有重试都失败,事件被标记为失败,并可能可供手动重放。
你的端点应快速响应 2xx 状态码以确认收到。如果你返回非 2xx 代码或超时,提供商将重试。
实现幂等性
因为重试可能导致重复交付,你的端点必须是幂等的。使用事件 ID(包含在负载或头部中)来跟踪已处理的事件。在处理之前,检查你是否已经处理过该 ID。将已处理的 ID 存储在具有 TTL 的数据库中。
幂等性检查示例:
def process_event(event_id, data):
if db.exists(f"processed:{event_id}"):
return # 已处理
# 处理数据
db.setex(f"processed:{event_id}", 86400, "1") # 24小时后过期
构建 Webhook 接收器:分步指南
- 暴露 HTTPS 端点:使用 TLS 保护传输中的数据。
- 验证签名:使用 401 拒绝签名无效的请求。
- 检查时间戳:如果提供,确保它在可接受的时间窗口内(例如 5 分钟)。
- 解析负载:验证 JSON 结构和必填字段。
- 检查幂等性:使用事件 ID 避免重复。
- 异步处理:立即返回 200,然后在后台作业中处理以避免超时。
- 记录和监控:记录失败并设置警报。
在本地测试 Webhook
在开发过程中,你需要一种在本地机器上接收 Webhook 的方法。像 ngrok 或 localtunnel 这样的工具会创建一个公共 URL,转发到你的本地主机。或者,许多提供商提供 CLI 来触发测试事件。
始终使用有效和无效签名测试签名验证。通过返回错误代码并观察提供商的重试行为来模拟重试。
比较:Webhook 与轮询
| 方面 | Webhook | 轮询 |
|---|---|---|
| 实时性 | 近乎实时 | 受间隔延迟 |
| 效率 | 仅在事件发生时发送数据 | 即使没有数据也频繁请求 |
| 复杂性 | 需要端点、签名验证、重试 | 实现更简单 |
| 可靠性 | 取决于重试机制 | 你控制调度 |
常见问题
什么是签名 Webhook 负载?
签名负载包含一个加密签名(通常是 HMAC),允许接收者验证请求来自预期的发送者且未被篡改。
Webhook 应该重试多少次?
大多数提供商使用指数退避重试 5-10 次。确切次数取决于提供商的政策。你的端点应该是幂等的以处理重复。
我的 Webhook 应该返回什么 HTTP 状态码?
返回 2xx 状态码(例如 200 OK)以确认成功接收。任何其他代码(4xx 或 5xx)表示失败并触发重试。
准备好测试你的 Webhook 负载了吗?使用我们的 JSON 格式化工具 快速验证和美化 JSON 负载。