Webhooks 詳解:簽名負載與重試策略
你已經建立了一個 API,現在需要在事件發生時通知其他服務。輪詢效率低下,因此你實作了 Webhooks。但很快你面臨兩個常見問題:如何確保負載未被篡改,以及當接收端點故障時該怎麼辦。本文說明如何使用簽名負載和重試策略,讓你的 Webhooks 既安全又可靠。
為什麼 Webhooks 需要簽名和重試
Webhooks 是由事件觸發的 HTTP 回呼。它們實作簡單,但引入了安全性和可靠性挑戰。沒有簽名,攻擊者可以偽造請求到你的端點,導致資料損壞或未授權的操作。沒有重試,暫時性故障(如網路波動或臨時伺服器錯誤)會導致事件遺失。
簽名負載解決了真實性問題:發送方使用密鑰對負載簽名,接收方進行驗證。重試解決了可靠性問題:發送方以退避策略重試失敗的交付。
簽名負載如何運作
簽名負載包含一個由請求主體和共享密鑰生成的加密簽名。最常見的方法是使用 SHA-256 的 HMAC(基於雜湊的訊息驗證碼)。
生成簽名
發送方使用密鑰計算原始請求主體的 HMAC。簽名會放在標頭中,通常還會附帶時間戳以防止重放攻擊。
// Node.js 範例:生成 HMAC 簽名
const crypto = require('crypto');
function signPayload(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return { timestamp, signature };
}
將時間戳和簽名包含在標頭中,例如 X-Webhook-Timestamp 和 X-Webhook-Signature。
驗證簽名
接收方必須使用收到的時間戳和原始主體重建簽名負載,然後計算 HMAC 並安全地進行比較。
// Node.js 範例:驗證簽名
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// 使用時間安全比較
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
始終使用時間安全比較以防止計時攻擊。同時,檢查時間戳是否為近期(例如 5 分鐘內)以減輕重放攻擊。
設計重試策略
重試對於可靠的 Webhook 交付至關重要。但簡單的重試可能會壓垮接收方或導致重複處理。良好的重試策略包括:
- 指數退避: 每次重試之間等待更長時間(例如 1 秒、2 秒、4 秒、8 秒...)。
- 最大重試次數: 限制嘗試次數(例如 5 次)以避免無限迴圈。
- 死信佇列: 達到最大重試次數後,儲存事件以供手動檢查。
- 冪等性: 包含唯一的事件 ID,以便接收方可以去重。
以下是虛擬碼中的簡單重試迴圈:
function deliverWithRetry(url, payload, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, { method: 'POST', body: payload });
if (response.ok) return;
} catch (error) {
// 記錄錯誤
}
attempt++;
const delay = Math.pow(2, attempt) * 1000; // 指數退避
await new Promise(resolve => setTimeout(resolve, delay));
}
// 移至死信佇列
}
考慮在退避中加入抖動,以避免驚群效應問題。
比較:輪詢 vs 帶重試的 Webhooks
| 方面 | 輪詢 | 帶重試的 Webhooks |
|---|---|---|
| 延遲 | 高(取決於間隔) | 低(近乎即時) |
| 伺服器負載 | 恆定(即使沒有事件) | 事件驅動 |
| 可靠性 | 高(接收方控制) | 取決於重試策略 |
| 安全性 | 標準 API 認證 | 需要簽名負載 |
逐步指南:實作帶簽名和重試的 Webhooks
- 為每個 Webhook 端點生成密鑰,並安全地共享。
- 事件發生時,建構負載(JSON)並使用 HMAC-SHA256 與密鑰和時間戳計算簽名。
- 發送 POST 請求 到端點,標頭包含:
Content-Type: application/json、X-Webhook-Timestamp和X-Webhook-Signature。 - 在接收方,使用原始主體和時間戳驗證簽名。如果無效或時間戳太舊則拒絕。
- 如果交付失敗(非 2xx 回應或網路錯誤),以指數退避重試,最多嘗試一定次數。
- 達到最大重試次數後,記錄失敗並可選地發出警報;將事件儲存在死信佇列中以供手動重放。
- 確保接收方的冪等性,使用事件 ID 忽略重複項。
常見陷阱與最佳實踐
- 未使用原始主體: 許多框架會解析 JSON 並更改主體。請使用原始主體進行簽名驗證。
- 忽略時間戳: 沒有時間戳驗證,可能遭受重放攻擊。
- 沒有冪等性: 重試可能導致重複處理。使用冪等鍵。
- 無限重試: 始終設定最大重試限制和死信佇列。
- 未記錄: 記錄交付嘗試和失敗以供除錯。
常見問題
什麼是簽名的 Webhook 負載?
簽名的 Webhook 負載包含一個加密簽名(例如 HMAC),允許接收方驗證請求來自預期的發送方且未被篡改。
我應該重試失敗的 Webhook 多少次?
常見做法是使用指數退避重試 3–5 次。確切次數取決於你的 SLA 和事件的關鍵性。始終為超過重試限制的事件使用死信佇列。
我可以使用沒有簽名的 Webhooks 嗎?
不建議。沒有簽名,你的端點容易受到偽造請求的攻擊。始終使用簽名(如 HMAC)並驗證時間戳以防止重放攻擊。
在除錯 Webhook 交付時,檢查伺服器日誌至關重要。如果你使用 Nginx,Nginx 日誌分析器 可以幫助你解析和分析存取日誌,以追蹤 Webhook 請求並識別失敗。