Webhook 解析:簽章載荷與重試邏輯

Backend2026-10-15TryQuickToolBox

你剛剛整合了一個支付閘道。文件上寫著:「當付款成功時,我們會發送一個 Webhook。」很好。但接著你想:我怎麼知道這個請求真的來自他們?如果他們發送時我的伺服器剛好掛掉怎麼辦?如果他們重複發送同一個事件呢?

Webhook 很強大,但也伴隨著一些陷阱。本指南將帶你了解其運作機制、安全性,以及你需要正確處理它們所需的可靠性模式。

什麼是 Webhook?

Webhook 是一種 HTTP POST 請求,當特定事件發生時,由一個系統發送給另一個系統。你的應用程式不需要每隔幾秒輪詢 API(「有新訂單嗎?有新訂單嗎?」),而是由服務提供者在事件發生時立即將資料推送給你。

典型流程:

  1. 你向服務提供者註冊一個 URL(例如 https://yourapp.com/webhooks/payments)。
  2. 事件發生(付款成功、使用者註冊、檔案上傳)。
  3. 服務提供者向你的 URL 發送一個 POST 請求,附帶描述該事件的 JSON 主體。
  4. 你的伺服器處理該事件並回應 2xx 以確認收到。

如果你沒有快速回應(通常在 5 到 30 秒內),服務提供者會認為傳遞失敗,並可能重試。

為什麼必須驗證簽章載荷

你的 Webhook 端點是公開可存取的。任何發現該 URL 的人都可以發送偽造事件。如果沒有驗證,攻擊者可以偽造一個「付款成功」事件並免費取得商品。

解決方案:簽章載荷。服務提供者使用共享密鑰(只有你和他們知道)對請求主體計算密碼學簽章,並將其包含在標頭中。你在自己這邊重新計算簽章並進行比對。

HMAC 簽章如何運作

大多數服務提供者使用 HMAC-SHA256。步驟如下:

  1. 服務提供者將原始請求主體與時間戳串接(以防止重放攻擊)。
  2. 服務提供者計算 HMAC-SHA256(secret, timestamp + '.' + body)。
  3. 服務提供者在標頭中發送簽章,例如 X-Signature: t=1234567890,v1=abc123...。
  4. 你提取時間戳和簽章,使用你的密鑰副本重新計算 HMAC,並使用恆定時間比較函數進行比對。

以下是一個最小的 Node.js 範例:

const crypto = require('crypto');

function verifySignature(req, secret) {
  const signatureHeader = req.headers['x-signature'];
  const [tPart, vPart] = signatureHeader.split(',');
  const timestamp = tPart.split('=')[1];
  const receivedSig = vPart.split('=')[1];

  const payload = timestamp + '.' + JSON.stringify(req.body);
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // Constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(receivedSig, 'hex'),
    Buffer.from(expectedSig, 'hex')
  );
}

重要: 務必使用原始請求主體進行簽章驗證。如果你的框架解析 JSON 並重新序列化,位元組序列可能不同,導致驗證失敗。請在解析之前捕獲原始主體。

時間戳驗證

檢查時間戳是否為近期(例如 5 分鐘內)。這可以防止重放攻擊,攻擊者會捕獲有效請求並在稍後重新發送。

重試邏輯:優雅地處理失敗

你的伺服器可能當機、超載或遇到網路暫時故障。服務提供者透過重試失敗的傳遞來處理這種情況。但你必須設計你的端點以安全地處理重試。

指數退避與抖動

服務提供者通常會以遞增的延遲進行重試:1 分鐘、5 分鐘、30 分鐘、2 小時等。這稱為指數退避。有些會加入隨機抖動以避免驚群效應。

從你的角度來看,你無法控制服務提供者的重試排程,但你應該:

冪等性:安全重試的關鍵

重試意味著你可能會多次收到同一個事件。你的處理必須是冪等的——處理同一個事件兩次應該與處理一次具有相同的效果。

常見做法:在載荷中包含一個唯一的事件 ID。在處理之前,檢查你是否已經處理過該 ID。如果是,則返回 200 並跳過。

async function handleWebhook(event) {
  const existing = await db.events.findOne({ id: event.id });
  if (existing) {
    return { status: 200, body: 'Already processed' };
  }

  await db.events.insert({ id: event.id, processedAt: new Date() });
  await processEvent(event);
  return { status: 200, body: 'OK' };
}

這種模式確保即使服務提供者發送事件三次,你也只會執行一次操作。

比較:輪詢 vs Webhook

面向 輪詢 Webhook
延遲 取決於間隔(例如 30 秒) 近乎即時
伺服器負載 恆定,即使沒有事件 僅在事件發生時
複雜度 易於實現 需要端點、驗證、重試
可靠性 你控制重試 取決於服務提供者的重試策略

Webhook 在即時更新方面勝出,但如果你擔心遺漏傳遞,輪詢可以作為關鍵事件的備用方案。

最佳實踐檢查清單

除錯 Webhook 問題

當 Webhook 失敗時,第一步是檢查原始請求。常見問題:

如果你正在除錯 HTTP 流量,像 Nginx Log Analyzer 這樣的工具可以幫助你發現請求失敗和回應碼中的模式。

常見問題

什麼是 Webhook 密鑰,我該如何儲存它?

密鑰是用於簽署載荷的共享金鑰。將其儲存在環境變數或密鑰管理器中——絕不要放在程式碼或版本控制中。如果服務提供者支援,請定期輪換它。

服務提供者會重試失敗的 Webhook 多少次?

這因服務提供者而異。有些會以指數退避重試 24 小時,其他則重試 3 天。請查看你的服務提供者文件以了解確切的排程和最大嘗試次數。

我可以將 Webhook 與無伺服器函數一起使用嗎?

可以,但要注意執行時間限制。快速確認 Webhook(返回 200)並將繁重處理卸載到佇列或背景函數以避免逾時。

準備好建立穩健的 Webhook 整合了嗎?從驗證簽章和實現冪等性開始——這兩個步驟將為你節省數小時的除錯時間,並防止代價高昂的安全事件。