Webhooks 詳解:簽章負載與重試機制
為什麼 Webhooks 很棘手
Webhooks 的概念很簡單:與其輪詢 API 來檢查變更,不如讓提供者透過 HTTP POST 將事件推送到你的端點。但在實務上,Webhooks 帶來了一系列挑戰,讓許多開發者頭痛:驗證請求確實來自提供者、處理重複傳送,以及應對網路故障。如果忽略這些問題,你可能會面臨安全漏洞、資料損毀和使用者不滿。
本文將探討生產級 Webhooks 的兩個最關鍵面向:簽章負載與重試。我們將涵蓋如何驗證簽章、如何使用指數退避實作重試,以及如何讓處理過程具備幂等性。
簽章負載:證明真實性
任何人都可以向你的端點發送 HTTP POST。為了確保請求來自你的提供者,提供者會使用密鑰對負載進行簽章,並在標頭中傳送簽章。你的任務是重新計算簽章並進行比對。
大多數提供者使用 HMAC-SHA256。簽章通常是對原始請求主體(有時包含時間戳)計算,並以十六進位編碼。例如,Stripe 使用類似 Stripe-Signature 的標頭,其中包含時間戳和簽章。
HMAC 如何運作
HMAC(基於雜湊的訊息驗證碼)將密鑰與雜湊函數結合。給定相同的密鑰和訊息,輸出是確定性的。沒有密鑰,攻擊者無法偽造有效的簽章。
以下是 Node.js 中的一個最小範例:
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)
);
}
在 Python 中同樣簡單:
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
簽章驗證的最佳實踐
- 使用恆定時間比較以防止計時攻擊。
crypto.timingSafeEqual和hmac.compare_digest都是安全的。 - 驗證原始主體,而不是解析後的 JSON 物件。解析和重新序列化可能會改變空白字元並破壞簽章。
- 在簽章資料中包含時間戳以防止重放攻擊。拒絕超過幾分鐘的請求。
- 安全地儲存密鑰——使用環境變數或密鑰管理服務,切勿硬編碼。
處理時間戳與重放攻擊
許多提供者會在簽章標頭中包含時間戳。例如,標頭可能是 t=1614556800,v1=abc123...。你應該:
- 提取時間戳和簽章。
- 檢查時間戳是否在可接受的時間窗口內(例如 5 分鐘)。
- 使用時間戳和負載重新計算簽章。
- 安全地比較簽章。
這可以防止攻擊者重放舊的有效請求。
重試:優雅地處理失敗
Webhooks 是透過公共網際網路傳送的,因此失敗是不可避免的。你的端點可能離線、緩慢或傳回 500 錯誤。一個穩健的提供者會重試失敗的傳送,但如果你是提供者,你也需要在自己的端處理重試。
帶有抖動的指數退避
重試時,避免立即重試對端點造成壓力。使用指數退避:每次失敗後等待更長時間。加入抖動(隨機性)以避免當許多 Webhooks 同時失敗時出現的驚群效應。
典型的重試排程可能如下所示:
| 嘗試次數 | 延遲(約) |
|---|---|
| 1 | 立即 |
| 2 | 30 秒 |
| 3 | 5 分鐘 |
| 4 | 30 分鐘 |
| 5 | 2 小時 |
達到最大嘗試次數後,將傳送標記為失敗並發出警報。
幂等性:安全重試的關鍵
由於重試可能導致重複傳送,你的 Webhook 處理程序必須是幂等的。這意味著多次處理同一個事件應該與處理一次具有相同的效果。
如何實現幂等性:
- 使用負載中提供的唯一事件 ID(例如
id欄位)。將已處理的 ID 儲存在具有唯一約束的資料庫中。 - 將操作設計為幂等——例如,使用 upsert 而不是 insert,或在建立資源之前檢查它是否已存在。
- 快速傳回 2xx,如果工作繁重則非同步處理。這減少了觸發重試的超時機會。
在提供者端實作重試
如果你正在建置 Webhook 系統,你需要一個可靠的傳送機制。以下是一個簡化的方法:
- 當事件發生時,將帶有負載和目標 URL 的傳送作業加入佇列。
- 工作人員提取作業並發送 HTTP POST,設定超時(例如 10 秒)。
- 如果回應狀態不是 2xx,則使用指數退避安排重試。
- 追蹤嘗試次數,並在達到最大值(例如 5 次)後放棄。
- 記錄失敗並提供使用者手動重放的方式。
使用持久性佇列(例如 Redis、RabbitMQ 或資料庫表),以便重試能在重新啟動後繼續。
綜合整理:Webhook 接收器檢查清單
建置 Webhook 接收器時,請遵循以下檢查清單:
- 驗證簽章,使用提供者的密鑰和恆定時間比較。
- 檢查時間戳以防止重放攻擊。
- 快速回應 2xx——在進行繁重處理之前確認收到。
- 如有需要,非同步處理,使用佇列。
- 確保幂等性,透過追蹤事件 ID。
- 記錄所有內容以供除錯和審計。
- 監控失敗並為重複重試設定警報。
常見問題
什麼是簽章 Webhook 負載?
簽章負載包含使用共享密鑰生成的加密簽章(通常是 HMAC-SHA256)。接收方重新計算簽章以驗證請求來自預期的發送方且未被篡改。
為什麼 Webhooks 需要重試?
網路問題、伺服器停機或暫時性錯誤可能導致 Webhook 傳送失敗。使用指數退避的重試可確保事件最終被送達,而不會壓垮接收方。
如何防止重複處理 Webhook?
使用幂等性:在負載中包含唯一事件 ID,儲存已處理的 ID,並將操作設計為幂等(例如 upsert)。這樣,即使 Webhook 被多次傳送,效果也是相同的。
如果你需要檢查或除錯 Webhook 負載,JSON Formatter 可以幫助你快速美化列印和驗證 JSON 資料。