Webhook 詳解:簽名載荷與重試機制
你剛剛整合了一個第三方支付提供商,它承諾在支付成功時通過 Webhook 通知你的應用。但你怎么知道請求真的來自他們,而不是攻擊者?如果你的服務器在 Webhook 觸發時短暫宕機,會發生什么?如果沒有適當的保護措施,你可能會處理虛假事件或丟失關鍵更新。本文解釋如何通過簽名載荷保護 Webhook,並實現健壯的重試機制。
什么是 Webhook?
Webhook 是一種用戶定義的 HTTP 回調。當源系統中發生事件(例如支付完成)時,它會向你提供的 URL 發送 HTTP POST 請求。與輪詢(反復請求更新)不同,Webhook 近乎實時地將數據推送給你。它們廣泛用於事件通知,例如 CI/CD 觸發、聊天消息或支付狀態變更。
為什么要用簽名保護 Webhook?
Webhook 端點是公開可訪問的,因此任何人都可以向它們發送 POST 請求。如果沒有驗證,攻擊者可以偽造事件,導致未授權的操作,例如將訂單標記為已支付。使用共享密鑰對載荷進行簽名可以讓你驗證真實性和完整性。
簽名載荷如何工作
發送方和接收方共享一個密鑰。對於每個 Webhook,發送方計算請求體的加密簽名(通常是 HMAC),並將其包含在標頭中。接收方使用相同的密鑰重新計算簽名並進行比較。如果匹配,則請求是真實的。
選擇正確的算法
HMAC-SHA256 是事實上的標準。它快速、安全,並且在所有主流語言中可用。避免使用被認為較弱的 MD5 或 SHA1。
逐步指南:驗證簽名載荷
- 從標頭中提取簽名(例如
X-Signature)。 - 獲取原始請求體作為字符串或字節數組。不要先解析它;任何轉換都會改變簽名。
- 使用你的密鑰和原始請求體計算 HMAC。
- 使用恆定時間比較將計算出的簽名與接收到的簽名進行比較,以防止時序攻擊。
以下是使用 Express 的 Node.js 示例:
const crypto = require('crypto');
const express = require('express');
const app = express();
// 為 webhook 路由使用原始請求體解析器
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) {
return res.status(401).send('Invalid signature');
}
// 處理事件
const event = JSON.parse(req.body);
// ... 處理事件
res.status(200).send('OK');
});
常見陷阱:在驗證之前使用 JSON 請求體解析器。Express 的 express.json() 會解析請求體,因此原始字節會丟失。始終為 Webhook 路由使用 express.raw() 或單獨捕獲原始請求體。
設計重試策略
即使有簽名,Webhook 也可能失敗:你的服務器可能宕機,或者請求可能超時。可靠的發送方會重試失敗的投遞。作為接收方,你也應該優雅地處理重試。
發送方重試
如果你正在構建發送方,請實現帶抖動的指數退避。例如,在 1 分鐘後重試,然後 5 分鐘,然後 30 分鐘,直到最大嘗試次數(例如 5 次)。在載荷中包含唯一的事件 ID,以便接收方可以去重。
接收方冪等性
由於重試可能導致重復投遞,你的端點必須是冪等的。使用事件 ID 跟蹤已處理的事件。將其存儲在具有唯一約束的數據庫中,或使用帶 TTL 的 Redis 等緩存。如果事件 ID 已見過,立即返回 200 OK 而不重新處理。
使用數據庫唯一索引的示例:
async function handleWebhook(event) {
try {
await db.query('INSERT INTO processed_events (event_id) VALUES ($1)', [event.id]);
} catch (err) {
if (err.code === '23505') { // 唯一約束違反
return; // 已處理
}
throw err;
}
// 處理事件
}
Webhook 端點的最佳實踐
- 快速響應:在幾秒內返回 2xx 狀態。將繁重的處理卸載到後台任務。
- 驗證時間戳:在簽名中包含時間戳以防止重放攻擊。拒絕超過幾分鐘的請求。
- 使用 HTTPS:始終使用 TLS 保護傳輸中的載荷。
- 記錄一切:記錄傳入的 Webhook,包括標頭和請求體,以便調試。
- 監控失敗:為重復失敗或簽名不匹配設置警報。
比較:簽名與未簽名 Webhook
| 方面 | 簽名 Webhook | 未簽名 Webhook |
|---|---|---|
| 安全性 | 高 – 驗證發送方和完整性 | 低 – 任何人都可以偽造請求 |
| 實現復雜度 | 中等 – 需要 HMAC 和密鑰管理 | 低 – 只需接受 POST |
| 使用場景 | 支付、敏感數據 | 非關鍵通知 |
常見問題
如何在 Python 中驗證 Webhook 簽名?
使用 hmac 模塊和 SHA256。使用 hmac.compare_digest 將計算出的摘要與標頭進行比較,以實現恆定時間比較。
如果我的 Webhook 端點收到重復事件該怎么辦?
檢查事件 ID 是否在已處理 ID 的存儲中。如果是重復的,返回 200 OK 並跳過處理。這確保了冪等性。
Webhook 發送方應執行多少次重試?
沒有固定數量,但通常使用指數退避進行 3–5 次重試(例如 1 分鐘、5 分鐘、30 分鐘)。之後,將投遞標記為失敗並發出警報。
準備測試你的 Webhook 端點?在開發過程中,使用我們的 JSON 格式化工具 來美化打印和驗證載荷。