Webhook 詳解:簽章載荷與重試機制
你剛剛整合了支付供應商的 Webhook。文件上寫著「當支付成功時,我們會向你的端點發送 POST 請求」。聽起來很簡單,對吧?然後你注意到重複的事件、遺漏的通知,以及關於偽造請求的安全警報。Webhook 概念簡單,但在生產環境中卻很棘手。本文解釋如何正確地實作 Webhook,使用簽章載荷和重試機制,讓你建構可靠且安全的整合。
什麼是 Webhook?
Webhook 是一種反向 API:你的應用程式不需要輪詢服務來取得更新,而是由服務透過 HTTP POST 將事件推送到你的端點。這種方式高效且近乎即時。常見用途包括支付通知、CI/CD 觸發和聊天整合。
但 Webhook 帶來了挑戰:你如何知道請求真的來自供應商?如果你的伺服器宕機了怎麼辦?如果同一個事件到達兩次怎麼辦?讓我們逐一解決這些問題。
使用簽章載荷保護 Webhook
任何人都可以向你的端點發送 HTTP 請求。如果沒有驗證,攻擊者可以偽造事件,導致欺詐或資料損壞。標準的解決方案是使用共享密鑰對載荷進行簽章。
HMAC 簽章如何運作
供應商和你共享一個密鑰。對於每個事件,供應商使用該密鑰計算請求主體的 HMAC(基於雜湊的訊息驗證碼),並在標頭中發送簽章(例如 X-Signature)。你在自己這邊重新計算 HMAC 並進行比較。如果匹配,則請求是真實且未被篡改的。
以下是使用 Flask 的 Python 最小範例:
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'your_shared_secret'
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
if not signature:
abort(400, 'Missing signature')
body = request.get_data()
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, 'Invalid signature')
# Process the event
return '', 200
始終使用 hmac.compare_digest 進行恆定時間比較,以防止時序攻擊。此外,在解析 JSON 主體之前驗證簽章,以避免不必要的處理。
簽章的最佳實踐
- 使用強密鑰:生成至少 32 位元組的隨機密鑰並安全儲存(例如環境變數)。
- 包含時間戳:為防止重放攻擊,供應商通常會在簽章載荷中包含時間戳。拒絕超過幾分鐘的請求。
- 定期輪換密鑰:在輪換期間支援多個活躍密鑰,以避免停機。
- 記錄簽章演算法:指定雜湊函數(SHA-256 是標準)和標頭名稱。
透過重試處理失敗
從供應商的角度來看,Webhook 是發後即忘的,但你的端點可能暫時不可用。為確保傳遞,供應商實施重試策略。作為接收方,你必須為重試和重複做好準備。
供應商使用的重試策略
大多數供應商使用指數退避進行重試:例如,立即重試,然後在 1 分鐘、5 分鐘、30 分鐘後重試,直到達到最大嘗試次數(通常為 5-10 次)。有些還允許你配置重試時間表。具體行為因供應商而異,因此請閱讀供應商的文件。
為重試設計你的端點
你的端點應該:
- 快速回應:在幾秒內以 2xx 狀態碼確認接收。將繁重的處理非同步進行(例如將事件排入佇列)。
- 具備冪等性:同一個事件可能會被多次傳遞。使用冪等鍵(通常在事件載荷或標頭中提供)進行去重。將已處理的事件 ID 儲存一段合理的時間。
- 返回適當的狀態碼:成功時返回 2xx,客戶端錯誤時返回 4xx(供應商可能不會重試),伺服器錯誤時返回 5xx(供應商會重試)。
Python 中冪等性檢查的範例:
processed_events = set() # In production, use a database or cache
@app.route('/webhook', methods=['POST'])
def webhook():
# ... signature verification ...
event = request.json
event_id = event['id']
if event_id in processed_events:
return '', 200 # Already processed
# Process event
processed_events.add(event_id)
return '', 200
測試你的 Webhook 實作
在上線之前,徹底測試你的端點:
- 模擬簽章請求:編寫單元測試,生成有效和無效的簽章。
- 測試重試場景:模擬失敗並確保你的端點處理重複事件。
- 使用供應商工具:許多供應商提供 CLI 或儀表板來發送測試事件。
- 監控和記錄:記錄傳入的事件(不含敏感資料),並為簽章失敗或高錯誤率設置警報。
比較:輪詢 vs Webhook
| 方面 | 輪詢 | Webhook |
|---|---|---|
| 延遲 | 取決於間隔(例如幾分鐘) | 近乎即時 |
| 伺服器負載 | 高(頻繁請求) | 低(僅在事件發生時) |
| 複雜性 | 實作簡單 | 需要簽章驗證和重試處理 |
| 可靠性 | 你控制重試 | 供應商控制重試;你必須處理重複 |
常見問題
如何驗證 Webhook 簽章?
使用共享密鑰計算原始請求主體的 HMAC,並使用恆定時間比較函數將其與簽章標頭進行比較。在處理載荷之前始終進行驗證。
我的端點應該返回什麼來確認 Webhook?
一旦你收到事件並將其排入佇列進行處理,就返回 2xx 狀態碼(例如 200 OK)。避免在請求處理程序中進行長時間處理,以防止逾時。
如何處理重複的 Webhook 傳遞?
使用事件載荷中的冪等鍵(或唯一事件 ID)來追蹤已處理的事件。將這些 ID 儲存在資料庫或快取中,如果 ID 之前已見過,則跳過處理。
準備好測試你的 Webhook 載荷了嗎?使用我們的 JSON Formatter 來美化列印和驗證傳入 Webhook 的 JSON 主體,讓除錯更容易。