Webhook 詳解:簽名載荷與重試機制
為何 Webhook 需要簽名與重試
你已經建立了一個端點來接收來自支付供應商或 SaaS 平台的 Webhook。在測試環境中一切正常,但在生產環境中你會面臨兩個問題:如何確認請求真的來自供應商?以及當你的伺服器宕機或回應緩慢時會發生什麼?沒有簽名載荷,任何人都可以偽造請求。沒有重試,一次網路波動就可能永久遺失一個事件。
本指南說明如何正確實作這兩者。我們將涵蓋 HMAC 簽名、驗證步驟、重試策略與冪等性——可靠處理 Webhook 的三大支柱。
Webhook 簽名如何運作
Webhook 簽名是一種密碼學雜湊,用以證明載荷是由知道共享密鑰的人所建立。大多數供應商使用 HMAC-SHA256。發送方對原始請求主體(有時包含時間戳)計算簽名,並將其放入標頭中。接收方重新計算並進行比較。
典型流程如下:
- 供應商與消費者在供應商的控制面板中設定共享密鑰。
- 供應商建立載荷,計算
HMAC-SHA256(secret, payload),並將其放入如X-Signature的標頭中發送。 - 消費者接收請求,讀取原始主體,重新計算 HMAC,並使用常數時間函數進行比較。
- 若匹配,則處理事件;否則以 401 拒絕。
重要: 始終使用接收到的原始請求主體,而非解析後再重新序列化的版本。JSON 序列化可能改變空白與鍵的順序,導致簽名失效。
加入時間戳以防止重放攻擊
僅有簽名載荷可能被攻擊者擷取有效請求後重放。為緩解此問題,在簽名資料中包含時間戳,並拒絕超過幾分鐘的請求。例如:
signed_payload = timestamp + "." + raw_body
signature = HMAC-SHA256(secret, signed_payload)
消費者檢查時間戳是否在可接受窗口內(例如 5 分鐘),以及簽名是否匹配。
實作簽名驗證
讓我們看一個使用 Flask 的 Python 具體範例。我們將驗證 X-Signature 標頭中發送的簽名。
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(401)
raw_body = request.get_data()
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
# 處理事件
event = request.json
# ... 處理事件 ...
return '', 200
重點:
- 使用
hmac.compare_digest(或等效方法)進行常數時間比較,以避免時間攻擊。 - 在任何解析之前,透過
request.get_data()存取原始主體。 - 快速返回 200;將耗時處理非同步進行。
設計重試策略
即使有簽名,Webhook 仍可能失敗:你的伺服器可能宕機、返回 500 或超時。供應商通常使用指數退避進行重試。作為消費者,你也應優雅地處理重試。
需考慮以下幾點:
- 冪等性: 每個事件應有唯一 ID。儲存已處理的 ID,以避免重試時重複處理。
- 快速回應: 在幾秒內以 2xx 狀態確認收到。將事件排入佇列進行背景處理。
- 你方的重試邏輯: 若你呼叫下游服務,實作帶退避與斷路器的重試。
大多數供應商在非 2xx 回應或超時時重試,通常持續長達 24 小時,間隔逐漸增加。請查閱供應商文件了解具體細節。
範例:冪等事件處理器
使用資料庫表追蹤已處理的事件 ID。以下是一個簡化模式:
def handle_event(event_id, payload):
if db.exists('processed_events', event_id):
return # 已處理
# 處理載荷
db.insert('processed_events', event_id)
# ... 實際工作 ...
對於高吞吐量,在事件 ID 上使用唯一約束並捕捉重複鍵錯誤。
比較:簽名與未簽名 Webhook
| 方面 | 簽名載荷 | 未簽名載荷 |
|---|---|---|
| 安全性 | 驗證發送方真實性 | 任何人都可以偽造請求 |
| 重放保護 | 可透過時間戳實現 | 無 |
| 實作工作量 | 中等(HMAC + 時間戳檢查) | 低 |
| 建議用於 | 所有生產 Webhook | 僅限內部測試 |
常見陷阱與最佳實踐
- 驗證前不要解析: 始終先對原始主體驗證簽名。
- 使用 HTTPS: 簽名保護完整性,但 HTTPS 保護機密性。
- 輪換密鑰: 在輪換期間支援多個活躍密鑰,以避免停機。
- 記錄失敗: 記錄簽名不匹配與重試以利除錯。
- 使用真實載荷測試: 使用 webhook.site 或供應商的 CLI 等工具模擬事件。
常見問題
什麼是簽名 Webhook 載荷?
簽名載荷包含密碼學簽名(通常為 HMAC),讓接收方驗證請求來自可信來源且未被篡改。
如何處理 Webhook 重試而不重複動作?
透過追蹤事件 ID 使處理器具冪等性。在處理前,檢查 ID 是否已處理;若是,則返回成功而不重複工作。
我可以使用沒有簽名的 Webhook 嗎?
不建議用於生產環境。沒有簽名,你無法區分合法請求與偽造請求。當可用時,始終驗證簽名。
在除錯 Webhook 載荷或分析伺服器日誌時,JSON Formatter 可協助你快速檢查與驗證 JSON 結構。