Webhook 详解:签名载荷与重试策略
你已经构建了一个 API,向用户的端点发送 Webhook。但随后你面临经典问题:如何确保载荷未被篡改?当端点宕机时会发生什么?如何防止重复处理?在本文中,我们将通过签名载荷和稳健的重试策略正面解决这些挑战。
为什么 Webhook 需要签名
Webhook 是通知其他系统事件的 HTTP 回调。由于它们通过公共互联网发送,因此容易受到拦截和欺骗。没有签名,恶意行为者可以向你的用户发送虚假 Webhook,触发意外操作。使用共享密钥对载荷进行签名可确保真实性和完整性。
签名载荷的工作原理
最常见的方法是使用 HMAC(基于哈希的消息认证码)与 SHA-256。发送方和接收方共享一个密钥。发送方对载荷(有时包括时间戳)计算签名,并将其包含在头部中。接收方重新计算签名并与接收到的签名进行比较。
分步指南:签名载荷
- 创建载荷:将事件数据序列化为 JSON。确保格式一致(例如,没有多余空格),因为签名是基于确切的字节计算的。
- 添加时间戳:在载荷或头部中包含 Unix 时间戳以防止重放攻击。接收方可以拒绝超过几分钟的请求。
- 计算 HMAC:使用你的密钥生成载荷字符串(或时间戳 + 载荷)的 HMAC-SHA256。
- 附加签名:在像
X-Signature这样的头部中发送它。
以下是 Python 示例:
import hmac
import hashlib
import json
import time
secret = b'your_shared_secret'
payload = {"event": "user.created", "data": {"id": 123}}
body = json.dumps(payload, separators=(',', ':'))
timestamp = str(int(time.time()))
signed_content = timestamp + '.' + body
signature = hmac.new(secret, signed_content.encode(), hashlib.sha256).hexdigest()
# 发送头部: X-Signature: signature, X-Timestamp: timestamp
验证签名
在接收端,你必须:
- 从头部提取时间戳和签名。
- 如果时间戳太旧(例如,>5 分钟),则拒绝以防止重放攻击。
- 使用相同的方法重新计算签名。
- 使用恒定时间函数进行比较以避免时序攻击。
Node.js 验证示例:
const crypto = require('crypto');
function verifySignature(req, secret) {
const signature = req.headers['x-signature'];
const timestamp = req.headers['x-timestamp'];
if (!signature || !timestamp) return false;
if (Date.now() / 1000 - timestamp > 300) return false; // 5 分钟
const body = JSON.stringify(req.body);
const signed = timestamp + '.' + body;
const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
设计重试策略
即使有签名,Webhook 也可能因网络问题、服务器错误或超时而失败。稳健的重试策略可确保交付,同时不会给接收方造成过大负担。
关键考虑因素
- 幂等性:接收方应优雅地处理重复事件。使用幂等键(例如,事件 ID)进行去重。
- 指数退避:在增加的延迟后重试(例如,1秒、5秒、30秒、5分钟、30分钟),给接收方时间恢复。
- 最大重试次数:设置限制(例如,5 次尝试),然后将 Webhook 标记为失败,或许提醒用户。
- 超时:为每次尝试设置合理的超时(例如,10 秒)。
实现重试
大多数 Webhook 发送方使用队列系统(例如,Redis、RabbitMQ 或数据库)来安排重试。以下是一个简化流程:
- 事件触发时,将带有载荷和目标 URL 的 Webhook 作业入队。
- 工作进程获取作业并尝试交付。
- 如果响应状态为 2xx,则标记为成功。
- 如果不是,则使用指数退避安排重试,直到最大尝试次数。
- 达到最大尝试次数后,标记为失败并通知。
重试计划示例:
| 尝试 | 延迟 |
|---|---|
| 1 | 立即 |
| 2 | 1 分钟 |
| 3 | 5 分钟 |
| 4 | 30 分钟 |
| 5 | 2 小时 |
Webhook 安全最佳实践
- 使用 HTTPS:始终通过 TLS 交付 Webhook 以防止窃听。
- 轮换密钥:定期轮换共享密钥,并在轮换期间支持多个活动密钥。
- 验证 URL:确保 Webhook URL 是 HTTPS 且不指向内部 IP(以防止 SSRF)。
- 日志和监控:保留交付尝试和失败的日志,用于调试和审计。
常见问题
什么是签名的 Webhook 载荷?
签名载荷包含一个加密签名(通常是 HMAC),允许接收方验证发送方的真实性以及载荷未被篡改。
应该重试失败的 Webhook 多少次?
没有一刀切的答案,但常见的方法是使用指数退避重试 3–5 次。考虑事件的关键性和接收方的容量。
我可以使用 JWT 作为 Webhook 签名吗?
是的,可以使用 JWT,但它更复杂。HMAC 更简单,足以满足大多数用例。如果你需要在签名本身中包含声明或过期时间,JWT 很有用。
准备好测试你的 Webhook 载荷了吗?使用我们的 JSON Formatter 在发送前验证和美化你的 JSON 载荷。