Webhooks 詳解:簽名負載與重試機制
你已經整合了支付閘道或 CI/CD 服務,現在需要接收即時事件。Webhook 是標準解決方案,但它們帶來兩大挑戰:驗證請求確實來自預期的發送方,以及在你的端點宕機或緩慢時處理失敗。本文解釋如何實現簽名的 Webhook 負載和穩健的重試邏輯,並提供實用的程式碼範例。
什麼是 Webhook?
Webhook 是一種使用者定義的 HTTP 回呼。每當事件發生時,提供者會向您指定的 URL 發送 HTTP POST 請求,而不是由您的應用程式輪詢 API 以獲取更新。這使得整合高效且近乎即時。
然而,Webhook 是基於推送的,這意味著您的端點必須公開可訪問、處理意外負載,並應對重複或惡意請求。這就是簽名和重試發揮作用的地方。
為什麼要簽名 Webhook 負載?
如果沒有驗證,任何知道您的 Webhook URL 的人都可以發送虛假事件。使用共享密鑰對負載進行簽名,可以讓您驗證發送方並確保負載未被篡改。
最常見的方法是使用 SHA-256 的 HMAC(基於雜湊的消息驗證碼)。提供者使用密鑰對原始請求主體計算簽名,並將其包含在標頭中(例如 X-Signature)。您的伺服器重新計算簽名並進行比較。
實現 HMAC 驗證
以下是使用 Express 在 Node.js 中的簡單範例:
const crypto = require('crypto');
const express = require('express');
const app = express();
// 使用原始主體進行簽名驗證
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// 處理事件
const event = JSON.parse(payload);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
app.listen(3000, () => console.log('Webhook server running'));
關鍵點:
- 使用原始主體: 將主體解析為原始位元組,而不是 JSON,因為任何轉換都會改變簽名。
- 時間安全比較: 使用
crypto.timingSafeEqual防止時間攻擊。 - 密鑰管理: 安全地存儲密鑰(環境變數、密鑰管理器)。
處理重試:讓 Webhook 可靠
網路會故障,伺服器會崩潰,部署會導致停機。可靠的 Webhook 系統必須重試失敗的傳遞。但簡單的重試可能導致重複處理和系統過載。
重試策略
常見的重試模式包括:
- 指數退避: 每次重試之間等待更長時間(例如 1 秒、2 秒、4 秒、8 秒...)。
- 最大重試次數限制: 在 N 次嘗試後停止(例如 5 次),並將傳遞標記為失敗。
- 抖動: 添加隨機性以避免驚群效應。
大多數提供者實現類似:在 24 小時內最多重試 5 次,採用指數退避。
冪等性:安全重試的關鍵
由於重試可能多次傳遞同一事件,您的端點必須是冪等的。在負載中包含唯一的事件 ID,並存儲已處理的 ID 以跳過重複。
使用簡單的記憶體集合範例(在生產環境中使用資料庫):
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
// ... 簽名驗證 ...
const event = JSON.parse(payload);
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
// 處理事件
processedEvents.add(event.id);
res.status(200).send('OK');
});
在生產環境中,使用帶有事件 ID 唯一約束的資料庫。
Webhook 消費者的最佳實踐
- 快速回應: 在進行繁重處理之前,用 2xx 狀態確認收到。將事件排入佇列以進行非同步處理。
- 驗證時間戳: 如果提供者包含時間戳,拒絕超過幾分鐘的請求以防止重放攻擊。
- 使用 HTTPS: 始終通過 HTTPS 暴露您的 Webhook 端點,以保護傳輸中的負載。
- 記錄一切: 記錄傳入請求、簽名和處理結果以進行除錯。
- 監控失敗: 為重複的傳遞失敗設置警報。
重試策略比較
| 策略 | 優點 | 缺點 |
|---|---|---|
| 固定間隔 | 易於實現 | 可能使正在恢復的系統過載 |
| 指數退避 | 在停機期間減少負載 | 如果暫時性故障會延遲傳遞 |
| 帶抖動的指數退避 | 防止同步重試 | 稍微複雜一些 |
常見問題
如何驗證 Webhook 簽名?
使用共享密鑰計算原始請求主體的 HMAC,並使用時間安全函數將其與簽名標頭進行比較。切勿使用簡單相等進行比較。
如果我的 Webhook 端點宕機了怎麼辦?
提供者應使用指數退避進行重試。確保您的端點快速返回 2xx 並非同步處理事件,以最小化停機影響。
我可以使用沒有簽名的 Webhook 嗎?
不建議。沒有簽名,您無法驗證發送方,使您的端點容易受到虛假事件的攻擊。當可用時始終使用簽名。
在測試 Webhook 負載時,您可能需要檢查 JSON 資料。使用 JSON Formatter 快速美化列印和驗證負載。