Webhooks 详解:签名载荷与重试机制
为什么 Webhooks 需要签名和重试
你已经构建了一个端点来接收来自支付提供商或 SaaS 平台的 Webhooks。在测试中一切正常,但在生产环境中你面临两个问题:如何知道请求确实来自提供商,以及当服务器宕机或响应缓慢时会发生什么?没有签名载荷,任何人都可以伪造请求。没有重试机制,一次网络波动就可能导致事件永久丢失。
本指南解释如何正确实现两者。我们将涵盖 HMAC 签名、验证步骤、重试策略和幂等性——可靠处理 Webhooks 的三大支柱。
Webhook 签名的工作原理
Webhook 签名是一种加密哈希,用于证明载荷是由知道共享密钥的人创建的。大多数提供商使用 HMAC-SHA256。发送方对原始请求体(有时包含时间戳)计算签名,并将其包含在请求头中。接收方重新计算并比较。
以下是典型流程:
- 提供商和消费者共享一个密钥(在提供商的控制面板中配置)。
- 提供商创建载荷,计算
HMAC-SHA256(secret, payload),并将其放在类似X-Signature的请求头中发送。 - 消费者接收请求,读取原始请求体,重新计算 HMAC,并使用恒定时间函数进行比较。
- 如果匹配,则处理事件;否则,返回 401 拒绝。
重要: 始终使用接收到的原始请求体,而不是解析后重新序列化的版本。JSON 序列化可能会改变空白和键的顺序,从而破坏签名。
添加时间戳以防止重放攻击
仅凭签名载荷,攻击者可以捕获有效请求并重放。为了缓解,在签名数据中包含时间戳,并拒绝超过几分钟的请求。例如:
signed_payload = timestamp + "." + raw_body
signature = HMAC-SHA256(secret, signed_payload)
消费者检查时间戳是否在可接受的时间窗口内(例如 5 分钟),并且签名匹配。
实现签名验证
让我们看一个使用 Flask 的 Python 具体示例。我们将验证 X-Signature 请求头中发送的签名。
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'your_shared_secret'
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
if not signature:
abort(401)
raw_body = request.get_data()
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
# 处理事件
event = request.json
# ... 处理事件 ...
return '', 200
关键点:
- 使用
hmac.compare_digest(或等效方法)进行恒定时间比较,以避免时序攻击。 - 在任何解析之前,通过
request.get_data()访问原始请求体。 - 快速返回 200;将繁重处理异步进行。
设计重试策略
即使有签名,Webhooks 也可能失败:你的服务器可能宕机、返回 500 或超时。提供商通常使用指数退避进行重试。作为消费者,你也应该优雅地处理重试。
以下是需要考虑的事项:
- 幂等性: 每个事件应具有唯一 ID。存储已处理的 ID,以避免重试时重复处理。
- 快速响应: 在几秒内以 2xx 状态确认接收。将事件排队进行后台处理。
- 你方的重试逻辑: 如果调用下游服务,请实现带退避和熔断器的重试。
大多数提供商在非 2xx 响应或超时时重试,通常持续长达 24 小时,间隔逐渐增加。请查看提供商文档了解具体细节。
示例:幂等事件处理器
使用数据库表跟踪已处理的事件 ID。以下是一个简化模式:
def handle_event(event_id, payload):
if db.exists('processed_events', event_id):
return # 已处理
# 处理载荷
db.insert('processed_events', event_id)
# ... 实际工作 ...
对于高吞吐量,在事件 ID 上使用唯一约束并捕获重复键错误。
比较:签名与未签名 Webhooks
| 方面 | 签名载荷 | 未签名载荷 |
|---|---|---|
| 安全性 | 验证发送方真实性 | 任何人都可以伪造请求 |
| 重放保护 | 可通过时间戳实现 | 无 |
| 实现工作量 | 中等(HMAC + 时间戳检查) | 低 |
| 推荐用于 | 所有生产 Webhooks | 仅内部测试 |
常见陷阱与最佳实践
- 验证前不要解析: 始终先对原始请求体验证签名。
- 使用 HTTPS: 签名保护完整性,但 HTTPS 保护机密性。
- 轮换密钥: 在轮换期间支持多个活动密钥,以避免停机。
- 记录失败: 记录签名不匹配和重试以便调试。
- 使用真实载荷测试: 使用 webhook.site 或提供商的 CLI 等工具模拟事件。
常见问题
什么是签名 Webhook 载荷?
签名载荷包含加密签名(通常是 HMAC),让接收方验证请求来自可信来源且未被篡改。
如何在不重复操作的情况下处理 Webhook 重试?
通过跟踪事件 ID 使处理器幂等。在处理之前,检查 ID 是否已处理;如果是,则返回成功而不重复工作。
我可以使用没有签名的 Webhooks 吗?
不建议用于生产环境。没有签名,你无法区分合法请求和伪造请求。当可用时,始终验证签名。
在调试 Webhook 载荷或分析服务器日志时,JSON Formatter 可以帮助你快速检查和验证 JSON 结构。