Webhooks详解:签名负载与重试机制
你刚刚集成了支付提供商的 webhook 来接收交易更新。测试中一切正常,但在生产环境中,你发现在流量高峰时事件丢失,或者当提供商重试时出现重复扣款。Webhook 概念简单但实践起来很棘手:它们跨越网络边界,可能被延迟、重复或欺骗。本文解释如何使用签名负载和智能重试策略构建健壮的 webhook 消费者和生产者。
什么是 Webhooks?
Webhooks 是由事件触发的用户定义的 HTTP 回调。当源系统发生事件(例如新订单)时,它会向您提供的 URL 发送 HTTP POST 请求。与轮询不同,webhook 近实时推送数据,减少延迟和服务器负载。
然而,webhook 带来了挑战:确保请求确实来自预期的发送方、优雅地处理失败以及避免重复处理。
使用签名负载保护 Webhooks
如果没有验证,任何人都可以向您的 webhook 端点发送虚假请求。签名负载通过包含一个您可以验证的加密签名来解决这个问题。
HMAC 签名的工作原理
发送方使用共享密钥和请求体计算基于哈希的消息认证码(HMAC)。签名在头部发送(例如 X-Signature)。您在本地重新计算 HMAC 并进行比较。
// Node.js 示例:验证 webhook 签名
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
始终使用像 timingSafeEqual 这样的恒定时间比较函数来防止时序攻击。
包含时间戳
为了防止重放攻击,在签名负载中包含时间戳,并拒绝超过几分钟(例如 5 分钟)的请求。时间戳应该是签名数据的一部分。
实现可靠的重试
网络问题或临时服务器错误可能导致 webhook 传递失败。健壮的发送方会使用指数退避重试失败的传递。
重试策略
- 指数退避: 每次重试之间等待更长时间(例如 1秒、2秒、4秒、8秒...)。
- 最大重试次数: 设置限制(例如 5 次尝试)以避免无限循环。
- 死信队列: 在最大重试后,存储事件以供手动检查。
- 幂等性: 包含唯一的事件 ID,以便接收方可以去重。
示例:Python 中带退避的重试
import time
import requests
from requests.exceptions import RequestException
def send_webhook(url, payload, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, timeout=5)
if response.status_code < 500:
return True # 成功或客户端错误(不重试)
except RequestException:
pass
time.sleep(2 ** attempt) # 指数退避
return False
Webhook 消费者的最佳实践
- 快速响应: 在处理之前用 2xx 状态确认接收。异步排队工作。
- 验证签名: 在信任负载之前始终验证签名。
- 处理重复: 使用事件 ID 确保幂等处理。
- 记录一切: 保留接收到的 webhook 日志以供调试。
比较:Webhooks 与轮询
| 方面 | Webhooks | 轮询 |
|---|---|---|
| 延迟 | 近实时 | 取决于间隔 |
| 服务器负载 | 低(仅推送) | 高(频繁请求) |
| 复杂性 | 较高(需要端点、安全) | 较低 |
| 可靠性 | 需要重试 | 固有重试 |
常见问题
什么是签名 webhook 负载?
签名负载包含一个加密签名(通常是 HMAC),允许接收方验证请求的真实性和完整性。
我应该重试失败的 webhook 多少次?
常见的做法是使用指数退避进行 3-5 次重试,但这取决于您的 SLA 和事件的关键性。
我可以不使用 HTTPS 使用 webhooks 吗?
不可以,始终使用 HTTPS 来保护负载和签名不被拦截。纯 HTTP 会暴露敏感数据。
在调试 webhook 负载时,您可能需要快速格式化 JSON。试试我们的 JSON 格式化工具 来美化和验证 JSON 响应。