Webhook 详解:签名负载与重试机制
为什么 Webhook 既强大又脆弱
Webhook 驱动着实时集成:支付通知、CI/CD 触发器、聊天消息。但它们带来两大挑战:安全性(如何确认请求来自预期的发送方?)和可靠性(如果接收方宕机了怎么办?)。本文将展示如何通过签名负载和重试策略同时解决这两个问题。
什么是 Webhook?
Webhook 是事件发生时由提供方向消费方发送的 HTTP POST 请求。与轮询不同,Webhook 以近实时方式推送数据,降低延迟和服务器负载。提供方必须确保请求真实可信;消费方必须可靠地处理它。
使用签名负载保护 Webhook 安全
签名负载使用密钥对请求体创建加密签名(通常为 HMAC-SHA256)。接收方重新计算签名,并与请求头中的签名进行比较。如果匹配,则负载真实且未被篡改。
如何生成和验证签名
以下是典型流程:
- 提供方和消费方共享一个密钥(例如通过仪表板)。
- 提供方计算
HMAC-SHA256(secret, payload),并将其放入类似X-Signature的请求头中发送。 - 消费方读取原始请求体,计算相同的 HMAC,并使用恒定时间函数进行比较。
Node.js 示例:
const crypto = require('crypto');
function verifySignature(secret, payload, signature) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const digest = hmac.digest('hex');
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
}
始终使用恒定时间比较以防止时序攻击。在验证之前切勿解析请求体——使用原始请求体中间件。
实现重试以确保可靠投递
即使有签名,网络故障或临时中断也可能导致 Webhook 投递失败。健壮的重试机制可确保最终投递。
重试策略
- 指数退避:每次重试之间等待更长时间(例如 1 秒、2 秒、4 秒、8 秒)。
- 最大尝试次数:限制重试次数(例如 5 次),避免无限循环。
- 死信队列:达到最大尝试次数后,存储事件以供人工检查。
- 幂等性:包含唯一事件 ID,使消费方可以忽略重复项。
Python 重试逻辑示例:
import time
import requests
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 == 200:
return True
except requests.RequestException:
pass
time.sleep(2 ** attempt) # exponential backoff
return False
Webhook 消费方最佳实践
- 快速返回 2xx;如有需要则异步处理。
- 在任何处理之前验证签名。
- 使用幂等键处理重复投递。
- 记录所有传入的 Webhook 以便调试。
- 监控失败情况,并在重复出错时发出警报。
对比:轮询 vs Webhook
| 方面 | 轮询 | Webhook |
|---|---|---|
| 延迟 | 高(基于间隔) | 低(实时) |
| 服务器负载 | 持续请求 | 仅在事件发生时 |
| 复杂度 | 简单 | 需要重试/安全机制 |
| 使用场景 | 小规模、更新不频繁 | 实时集成 |
常见问题
什么是签名的 Webhook 负载?
签名负载包含加密签名(例如 HMAC),使接收方能够验证请求来自可信来源且未被篡改。
失败的 Webhook 应该重试多少次?
没有通用数字,但 3–5 次配合指数退避是常见做法。之后,记录事件以供人工审查。
我可以用 JWT 代替 HMAC 来签名 Webhook 吗?
可以,一些提供方使用 JWT。原理相同:在信任负载之前验证令牌的签名和声明。
需要检查 Webhook 负载或日志?试试我们的 JSON Formatter,快速美化和验证 JSON 负载。