Webhook 詳解:簽章驗證與重試機制
你剛剛整合了一個支付閘道。文件上寫著:「當付款成功時,我們會向你的端點發送一個 POST 請求。」聽起來很簡單,對吧?你建立了一個簡單的路由來接收 JSON、更新資料庫,然後回傳 200。然後事情發生了:客戶的付款成功了,但你的伺服器短暫停機。Webhook 從未送達。或者更糟的是,它送了兩次,而你重複入帳了款項。Webhook 看起來很簡單,直到它出錯為止。
本文將說明 Webhook 的實際運作方式、為什麼簽章載荷對安全性至關重要,以及如何建立重試邏輯來確保你的整合保持可靠——即使出了問題也一樣。
什麼是 Webhook?
Webhook 是一種 HTTP 回呼:與其讓你的應用程式輪詢 API 來偵測變更,提供方會在事件發生時向你指定的 URL 發送 HTTP POST。它是推送而非拉取。
常見的例子包括付款確認(Stripe、PayPal)、CI/CD 建置通知(GitHub、GitLab),以及訊息平台(Slack、Discord)。模式很簡單:你註冊一個 URL,提供方將 JSON 載荷發送到該 URL,你的伺服器負責處理它們。
但這種簡單性隱藏了兩個關鍵挑戰:真實性(你如何知道請求確實來自提供方?)和可靠性(如果你的端點停機或回應緩慢會怎樣?)。
為什麼簽章載荷很重要
你的 Webhook 端點是一個公開的 URL。任何人都可以向它發送 POST 請求。如果沒有驗證,攻擊者可以偽造付款成功事件、觸發出貨,或破壞你的資料。
簽章載荷透過在請求標頭中包含加密簽章來解決這個問題。提供方使用共享密鑰和請求主體計算 HMAC(基於雜湊的訊息驗證碼),然後將其與載荷一起發送。你的伺服器重新計算 HMAC 並進行比對。如果它們相符,則該請求是真實的。
HMAC 簽章如何運作
- 提供方與你共享一個密鑰(在設定時提供)。
- 對於每個 Webhook,提供方將時間戳和原始請求主體串接起來,然後使用密鑰計算 HMAC-SHA256 雜湊。
- 簽章會放在標頭中發送,通常類似
X-Signature或Stripe-Signature。 - 你的伺服器使用相同的密鑰重新計算 HMAC,並與接收到的簽章進行比對。
- 如果它們相符,你就處理該事件;否則,以 401 拒絕。
計算 HMAC 時務必使用原始請求主體。如果你的框架在驗證之前就解析了 JSON,主體可能會被改變(空白字元、鍵的順序),導致驗證失敗。
範例:在 Node.js 中驗證 Webhook 簽章
const crypto = require('crypto');
const express = require('express');
const app = express();
// 使用原始主體進行簽章驗證
app.post('/webhook', express.raw({ type: 'application/json' }), (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.toString());
// ... 處理事件
res.status(200).send('OK');
});
注意使用了 timingSafeEqual 來防止時序攻擊。切勿使用簡單的 === 來比對簽章。
設計重試邏輯
即使有簽章,Webhook 仍然可能失敗。你的伺服器可能停機、網路可能丟失請求,或者你的處理可能超時。提供方通常會重試失敗的傳遞,但你也應該設計系統來優雅地處理重試。
什麼算作失敗?
大多數提供方將任何非 2xx 的回應視為失敗。這包括 3xx 重定向、4xx 客戶端錯誤和 5xx 伺服器錯誤。有些還會強制執行超時(例如 5 秒)。如果你沒有快速回應,該次傳遞就會被標記為失敗並重試。
指數退避與抖動
提供方通常使用指數退避進行重試:第一次重試在 1 分鐘後,然後 5 分鐘、30 分鐘,依此類推。加入抖動(隨機變化)可以防止大量 Webhook 同時失敗時造成的驚群效應。
如果你正在建立 Webhook 發送器,請像這樣實作退避:
- 重試 1:10 秒後
- 重試 2:1 分鐘後
- 重試 3:5 分鐘後
- 重試 4:30 分鐘後
- 重試 5:2 小時後
在達到最大嘗試次數後,將事件移至死信佇列以供手動檢查。
幂等性:處理重複傳遞
重試意味著你的端點可能會多次收到相同的事件。你的處理必須是幂等的:處理相同事件兩次應該與處理一次具有相同的效果。
常見策略:
- 事件 ID 去重:將已處理的事件 ID 儲存在資料庫或快取中。在處理之前,檢查該 ID 是否已經出現過。
- 使用 Upsert 而非 Insert:使用能自然處理重複的資料庫操作(例如
INSERT ... ON CONFLICT DO NOTHING)。 - 幂等鍵:對於像扣款這類操作,傳遞一個唯一的鍵,讓支付提供方忽略重複的請求。
Webhook 消費者的最佳實踐
| 實踐 | 為何重要 |
|---|---|
| 驗證簽章 | 防止偽造請求 |
| 快速回應(2 秒內) | 避免超時和不必要的重試 |
| 非同步處理 | 將接收與繁重處理解耦 |
| 使用幂等處理器 | 安全應對重複傳遞 |
| 記錄所有事件 | 有助於除錯和稽核 |
| 監控失敗 | 在問題升級前偵測到它們 |
同時考慮驗證簽章中的時間戳以防止重放攻擊。許多提供方會在簽章載荷中包含時間戳;拒絕超過幾分鐘的舊事件。
Webhook 生產者的最佳實踐
如果你正在建立發送 Webhook 的服務,請遵循以下準則:
- 使用 HMAC-SHA256 對每個載荷進行簽章。
- 包含唯一的事件 ID 和時間戳。
- 使用指數退避和抖動進行重試。
- 為消費者提供查看傳遞日誌和手動重放事件的方式。
- 清楚記錄你的簽章驗證流程。
常見問題
什麼是簽章 Webhook 載荷?
簽章載荷包含一個加密簽章(通常是 HMAC),該簽章由請求主體和共享密鑰計算得出。接收方驗證簽章以確保請求是真實且未經修改的。
Webhook 應該重試多少次?
沒有通用的次數,但大多數提供方會在幾小時內重試 3 到 5 次。之後,事件通常會被標記為失敗,並可能可供手動重放。無論重試次數為何,你的系統都應該以幂等方式處理重複。
我可以不使用 HTTPS 來使用 Webhook 嗎?
技術上可以,但你應該始終使用 HTTPS。Webhook 通常包含敏感資料,如果沒有 TLS,攻擊者可以攔截或篡改載荷。HTTPS 也能確保你發送到預期的伺服器。
使用 Nginx 日誌除錯 Webhook
當 Webhook 失敗時,第一個要看的地方是你的網頁伺服器日誌。如果你使用 Nginx,存取和錯誤日誌會記錄每個請求,包括狀態碼和回應時間。你可以快速發現模式:重複的 401(簽章驗證失敗)、500(應用程式錯誤)或超時。
若要更快速地分析,請試試 Nginx Log Analyzer 來解析和彙總 Webhook 流量、按狀態碼篩選,並找出有問題的端點。