Webhook 詳解:簽名負載與重試機制
為何 Webhook 需要簽名負載與重試
您剛剛整合了一個會向您的端點發送 Webhook 的第三方服務。在測試環境中一切正常,但在正式環境中,您發現事件遺失、重複傳遞,更糟的是——有人偽造請求發送到您的 Webhook URL。如果沒有適當的安全與可靠性措施,Webhook 可能成為錯誤和漏洞的來源。
Webhook 是 HTTP 回呼,當事件發生時通知您的應用程式。概念簡單,但實作上卻很棘手,因為它們跨越了信任邊界和網路可靠性問題。在本指南中,我們將涵蓋兩個關鍵面向:簽名負載用於驗證真實性,以及重試以確保傳遞。我們將逐步介紹您可以立即應用的實作方式。
了解簽名負載
簽名負載是發送方證明 Webhook 來自他們且未被篡改的方式。發送方使用共享密鑰對請求主體(有時是標頭)計算加密簽名,然後將該簽名包含在標頭中。您的端點重新計算簽名並進行比較。
最常見的演算法是 HMAC-SHA256。它快速、安全且被廣泛支援。運作方式如下:
- 發送方和接收方共享一個密鑰(在 Webhook 設定中配置)。
- 對於每個 Webhook,發送方使用 HMAC-SHA256 以密鑰對負載進行雜湊,建立簽名。
- 簽名透過如
X-Signature的標頭發送。 - 接收方計算相同的 HMAC,並使用恆定時間比較來防止時序攻擊。
在 Node.js 中實作 HMAC 驗證
讓我們看一個具體範例。假設發送方在 X-Signature 中以十六進位字串包含簽名。以下是在 Express 路由中驗證的方法:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const expected = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(req.body);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
app.listen(3000);
重要: 始終使用原始請求主體進行簽名計算,而非解析後的 JSON。解析並重新字串化可能會改變空白和順序,破壞簽名。
時間戳與重放保護
僅有簽名無法防止重放攻擊。攻擊者可以擷取有效的簽名請求並稍後重新發送。為了緩解此問題,在簽名負載或標頭中包含時間戳。例如,新增一個包含 Unix 時間的 X-Timestamp 標頭。接收方檢查時間戳是否在容許視窗內(例如 5 分鐘),並拒絕較舊的請求。
同時,將時間戳包含在簽名計算中以防止篡改。發送方對 timestamp + '.' + body 計算 HMAC。接收方執行相同操作並進行比較。
實作重試以實現可靠傳遞
Webhook 透過公共網際網路傳輸,因此失敗是不可避免的:網路逾時、伺服器錯誤(5xx),或您的端點暫時停機。穩健的 Webhook 系統必須重試失敗的傳遞。
重試策略
常見的重試策略包括:
- 固定延遲: 在固定間隔後重試(例如 1 分鐘)。簡單,但若發生大量失敗可能會不堪負荷。
- 指數退避: 每次嘗試後等待更長時間(例如 1 秒、2 秒、4 秒、8 秒...)。這是最常見且推薦的方法。
- 抖動: 在延遲中新增隨機性,以避免大量 Webhook 同時失敗時出現的驚群效應。
典型的策略可能是:最多重試 5 次,採用指數退避和抖動,從 10 秒開始。之後,將傳遞標記為失敗,並可能發出警報。
冪等性:處理重複
重試可能導致重複傳遞。您的端點必須是冪等的——處理相同事件兩次不應造成副作用,例如重複向客戶收費。在負載中包含唯一的事件 ID(例如 event.id),並在合理期間內儲存已處理的 ID。在處理之前,檢查該 ID 是否已見過。
或者,將您的操作設計為自然冪等(例如使用 upsert 而非 insert)。
快速回應
您的 Webhook 端點應快速確認接收(在幾秒內),並以非同步方式處理事件。如果處理時間過長,發送方可能會逾時並重試,導致重複。使用佇列:在驗證簽名後立即回應 200 OK,然後將事件排入佇列進行背景處理。
Webhook 安全措施比較
| 措施 | 目的 | 實作複雜度 |
|---|---|---|
| HMAC 簽名 | 驗證真實性和完整性 | 低 |
| 時間戳驗證 | 防止重放攻擊 | 低 |
| 冪等鍵 | 避免重複處理 | 中 |
| IP 允許清單 | 限制為已知的發送方 IP | 低(但脆弱) |
| HTTPS | 加密傳輸中的資料 | 低 |
雖然 IP 允許清單可以增加一層防護,但並不可靠,因為發送方 IP 可能會變更。主要應依賴簽名。
測試您的 Webhook 端點
使用如 ngrok 的工具公開您的本機伺服器,並從提供者的儀表板觸發測試 Webhook。驗證簽名是否正確驗證,並透過模擬失敗(例如前幾次嘗試傳回 500)來確認重試是否有效。
偵錯時,您可能需要檢查 JSON 負載。JSON Formatter 可以幫助您快速美化列印並驗證 Webhook 負載。
常見問題
什麼是簽名 Webhook 負載?
簽名負載包含加密簽名(通常是 HMAC),讓接收方能夠驗證請求來自預期的發送方且未被更改。它是使用共享密鑰計算的。
失敗的 Webhook 應該重試幾次?
沒有通用的次數,但通常重試 3–5 次並採用指數退避。之後,記錄失敗並考慮發出警報。目標是處理暫時性問題,而不是無限重試。
我可以使用 JWT 作為 Webhook 簽名嗎?
可以,有些提供者使用 JWT(JSON Web Tokens)來簽署 Webhook。權杖包含聲明並使用密鑰或私鑰簽署。驗證方式類似:驗證簽名並檢查如到期時間等聲明。
準備好實作安全的 Webhook 了嗎?首先產生強密鑰,新增 HMAC 驗證,並設定具有冪等性的重試。若需要在開發期間快速檢查負載,試試我們的 JSON Formatter 來格式化和驗證 Webhook JSON。