Webhooks 詳解:簽章負載與重試機制
你剛整合完一個支付閘道,現在需要在付款成功時更新資料庫。每隔幾秒輪詢 API 既浪費資源又緩慢。Webhooks 透過在事件發生時將其推送至你的伺服器來解決這個問題。但若缺乏適當的安全性與可靠性,webhooks 可能成為錯誤與漏洞的來源。本文說明如何正確實作 webhooks,重點放在簽章負載與重試機制。
什麼是 Webhooks?
Webhook 是一種 HTTP 回呼:當服務中發生事件時,該服務會向你提供的 URL 發送 HTTP POST 請求。負載通常包含關於該事件的 JSON 資料。例如,當客戶完成付款時,Stripe 會向你的端點發送 charge.succeeded 事件。
Webhooks 採用推送式機制,因此與輪詢相比可降低延遲與伺服器負載。然而,它們也帶來挑戰:你如何確認請求是真實的?如果事件觸發時你的伺服器正好停機怎麼辦?
為什麼簽章負載很重要
任何人都可以向你的 webhook 端點發送 HTTP POST。若未經驗證,攻擊者可以偽造事件,導致未經授權的操作,例如將訂單標記為已付款。簽章負載透過包含只有發送方與你能產生的加密簽章來解決此問題。
最常見的方法是使用共享密鑰的 HMAC(雜湊式訊息驗證碼)。發送方使用密鑰計算負載的雜湊值,並將其包含在標頭中。你重新計算雜湊值並進行比對。若兩者相符,則該請求是可信的。
如何驗證簽章負載
以下是使用 Node.js 與 Express 的逐步做法:
- 以字串形式取得原始請求主體。驗證前請勿將其解析為 JSON,因為解析可能會改變位元組序列。
- 從標頭中提取簽章(例如
X-Signature)。 - 使用你的密鑰與原始主體計算 HMAC。
- 使用恆定時間比較法將計算出的簽章與收到的簽章進行比對,以防止時序攻擊。
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
res.status(200).send('OK');
});
請務必使用強密鑰並定期輪換。切勿在客戶端程式碼中暴露密鑰。
實作帶退避的重試機制
Webhooks 透過網際網路傳遞,因此失敗在所難免。你的伺服器可能停機,或網路可能出現暫時性問題。穩健的 webhook 系統會以指數退避方式重試失敗的傳遞。
通常,若發送方在逾時時間內(例如 5 秒)未收到 2xx 回應,便會進行重試。重試排程可能為:立即重試,然後在 1 分鐘、5 分鐘、30 分鐘、2 小時後等,直到達到最大嘗試次數(例如 5 次)。
作為接收方,你必須快速回應以避免逾時。以 200 OK 確認收到,並以非同步方式處理事件。
處理幂等性
由於重試可能導致重複傳遞,你的事件處理必須具備幂等性。在負載中包含唯一的事件 ID,並儲存已處理的 ID。處理前,檢查該 ID 是否已被處理過。
async function processEvent(event) {
const { id, type, data } = event;
if (await db.processedEvents.findOne({ id })) {
return; // Already processed
}
await db.processedEvents.insertOne({ id });
// Handle event based on type
}
使用資料庫交易來確保事件僅在成功處理後才被標記為已處理。
Webhook 安全性的最佳實踐
- 務必驗證簽章後再進行處理。
- 使用 HTTPS 以防止竊聽。
- 驗證時間戳記(若包含在內),以防止重放攻擊。
- 限制負載大小以避免阻斷服務攻擊。
- 記錄所有 webhook 嘗試以便除錯與稽核。
比較:輪詢 vs Webhooks
| 面向 | 輪詢 | Webhooks |
|---|---|---|
| 延遲 | 高(基於間隔) | 低(近乎即時) |
| 伺服器負載 | 高(持續請求) | 低(僅在事件發生時) |
| 複雜度 | 實作簡單 | 需要安全性與重試機制 |
| 可靠性 | 取決於輪詢頻率 | 取決於重試邏輯 |
常見問題
如何在本機測試 webhooks?
使用 ngrok 等工具將你的本機伺服器暴露至網際網路。設定 webhook 提供者將事件發送至你的 ngrok URL。
如果 webhook 發送時我的伺服器正好停機怎麼辦?
發送方應以退避方式重試。確保你的伺服器具備高可用性並快速回應,以避免逾時。
我可以使用非對稱簽章代替 HMAC 嗎?
可以,部分提供者使用 RSA 或 ECDSA 簽章。你使用他們的公鑰進行驗證。HMAC 較簡單,但需要共享密鑰。
在除錯 webhook 負載時,將 JSON 格式化以便閱讀會很有幫助。試試我們的 JSON Formatter,立即美化列印並驗證 webhook 負載。