Webhook 詳解:簽章負載與重試機制
你剛整合了一個支付閘道。文件上寫著:「當付款成功時,我們會發送 webhook 到你的端點。」你建立了一個簡單的路由,在本機測試過後就部署上線。然後在正式環境中,你開始注意到重複的事件、遺漏的通知,以及神秘的 500 錯誤。Webhook 看起來很簡單——直到它不再簡單。
本指南將說明 webhook 的實際運作方式、為什麼簽章負載很重要,以及如何建立重試邏輯,讓你的系統即使出錯也能保持同步。
什麼是 Webhook?
Webhook 是一種 HTTP 回呼:當特定事件發生時,某個系統會向你提供的 URL 發送 HTTP POST 請求。與輪詢(你不斷詢問「有發生什麼事嗎?」)不同,webhook 會以接近即時的方式將事件推送給你。
常見用途包括:
- 支付處理器通知你成功的扣款。
- Git 託管服務在 push 時觸發 CI/CD 流程。
- 通訊平台通知你有新訊息。
發送方(例如 Stripe、GitHub)是 webhook 提供者;你的伺服器則是 webhook 消費者。
為什麼簽章負載不可或缺
你的 webhook 端點是公開可存取的。任何知道 URL 的人都可以發送假事件。如果沒有驗證,攻擊者就能偽造「付款成功」事件,而你可能會免費出貨。
簽章負載透過在請求標頭中加入密碼學簽章來解決這個問題,該簽章使用只有你和提供者知道的共享密鑰產生。當你收到 webhook 時,重新計算簽章並進行比對。如果相符,表示負載是真實的。
HMAC 簽章的運作方式
大多數提供者使用 HMAC-SHA256。提供者會計算:
signature = HMAC-SHA256(secret, payload)並將其放在如 X-Signature 的標頭中發送。你的伺服器執行相同的計算並檢查是否相等。由於密鑰是保密的,只有提供者能產生有效的簽章。
實際驗證簽章
以下是使用 Express 的簡易 Node.js 範例:
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; // raw Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (signature !== expected) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
app.listen(3000);重要:務必使用原始請求主體進行簽章驗證。如果你先解析 JSON,序列化後的字串可能會不同(空白、鍵的順序),導致簽章檢查失敗。
常見陷阱
- 使用錯誤的密鑰:有些提供者為測試和正式模式使用不同的密鑰。
- 時序攻擊:使用固定時間的比較函式(例如
crypto.timingSafeEqual)以避免洩漏資訊。 - 忽略時間戳標頭:許多提供者會包含時間戳以防止重放攻擊。拒絕超過數分鐘的請求。
重試:優雅地處理失敗
網路會故障、伺服器會重啟,部署也會發生。Webhook 提供者預期你的端點可能暫時無法使用,因此會重試失敗的傳遞。但重試會帶來新的挑戰:重複事件、亂序傳遞,以及如果不小心的話可能出現的無限迴圈。
提供者如何重試
大多數提供者使用指數退避:第一次失敗後等待短時間,然後間隔逐漸拉長。例如,Stripe 會以遞增的延遲重試最多 3 天。GitHub 最多重試 8 次。確切的排程各不相同,但模式是一致的。
你的端點應快速回傳 2xx 狀態碼以確認收到。任何其他狀態(4xx、5xx)都表示失敗並觸發重試。
建立冪等處理器
由於重試可能多次傳遞相同事件,你的處理器必須是 冪等 的:處理相同事件兩次應與處理一次有相同的效果。
常見策略:
- 事件 ID 追蹤:將唯一事件 ID(在負載或標頭中提供)儲存在資料庫中。在處理前,檢查是否已處理過。
- Upsert:使用天生冪等的資料庫操作(例如
INSERT ... ON CONFLICT DO NOTHING)。 - 狀態機:設計你的業務邏輯以容忍重複的狀態轉換(例如,僅在尚未付款時「將訂單標記為已付款」)。
使用簡單記憶體集合的範例(僅供說明;正式環境請使用持久化儲存):
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const event = req.body;
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
// Process event...
processedEvents.add(event.id);
res.status(200).send('OK');
});快速回應
提供者通常有逾時限制(例如 5 秒)。如果你的處理器需要大量處理,請立即確認並以非同步方式處理。例如,將事件推送到佇列並回傳 200。
可靠 Webhook 的最佳實務
- 務必驗證簽章,使用原始主體和固定時間比較。
- 使用 HTTPS 保護你的端點,防止竊聽和竄改。
- 實作冪等性以處理重複傳遞。
- 記錄所有內容——傳入的負載、簽章和你的回應——以便除錯。
- 監控失敗並為重複錯誤設定警示。
- 使用真實事件測試,透過提供者儀表板或 CLI 工具。
- 處理亂序事件,在邏輯中加入時間戳或序號。
比較:輪詢 vs. Webhook
| 面向 | 輪詢 | Webhook |
|---|---|---|
| 延遲 | 取決於間隔 | 接近即時 |
| 伺服器負載 | 高(持續請求) | 低(僅事件發生時) |
| 複雜度 | 易於實作 | 需要端點、安全性、重試 |
| 可靠性 | 你控制重試 | 提供者控制重試 |
常見問題
什麼是 webhook 簽章?
Webhook 簽章是負載的密碼學雜湊(通常是 HMAC-SHA256),使用共享密鑰產生。它讓接收方驗證請求來自預期的發送方,且未被竄改。
如何處理重複的 webhook 事件?
透過追蹤事件 ID 或使用天生冪等的資料庫操作,讓你的處理器具備冪等性。在套用變更前,檢查事件是否已處理過。
我的 webhook 端點應該回傳什麼?
在你收到並驗證事件後,盡快回傳 2xx 狀態碼(例如 200 OK)。如果需要大量處理,請立即確認並以非同步方式處理工作。
準備好測試你的 webhook 負載了嗎?使用我們的 JSON Formatter 來檢查並驗證傳入事件的 JSON 結構。