Webhooks 詳解:簽名載荷與重試機制

Backend2026-10-18TryQuickToolBox

你已經建立了一個 API,現在需要在事件發生時通知其他服務。你設置了一個 webhook 端點,但很快發現任何人都可以發送偽造載荷,而且當你的伺服器宕機時,事件會永遠丟失。這是一個常見的陷阱:webhooks 概念簡單,但需要仔細設計以確保安全性和可靠性。

在本指南中,我們將介紹 webhooks 的基本要素:如何簽名載荷以防止篡改、如何在接收端驗證簽名,以及如何實作指數退避重試邏輯來處理失敗。我們還會涵蓋冪等性以避免重複處理。最後,你將擁有一個值得信賴的穩健 webhook 系統。

什麼是 Webhooks?

Webhooks 是使用者定義的 HTTP 回呼。當來源系統中發生事件(例如付款處理完成)時,它會向您指定的 URL 發送 HTTP POST 請求,其中包含事件詳細資訊的載荷。這允許服務之間進行即時通訊,而無需輪詢。

然而,webhooks 引入了兩個挑戰:

使用簽名載荷保護 Webhooks

為了防止偽造請求,發送方應使用共享密鑰對載荷進行簽名。接收方隨後可以驗證簽名以驗證發送方的身份。最常見的方法是使用 SHA-256 的 HMAC(基於雜湊的消息驗證碼)。

HMAC 簽名如何運作

  1. 發送方和接收方共享一個密鑰(理想情況下是長隨機字串)。
  2. 對於每個 webhook,發送方使用密鑰計算請求主體的 HMAC,並將其包含在標頭中(例如 X-Signature)。
  3. 接收方對收到的主體計算相同的 HMAC,並與標頭值進行比較。如果匹配,則請求是真實的。

關鍵是要對原始請求主體進行簽名,而不是解析後的版本,以避免因 JSON 序列化差異而產生不一致。

範例:在 Node.js 中簽名載荷

const crypto = require('crypto');

function signPayload(payload, secret) {
  return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}

// 發送方:
const payload = JSON.stringify({ event: 'payment.success', amount: 100 });
const signature = signPayload(payload, 'your-secret-key');
// 在標頭中包含簽名:X-Signature: signature

在接收方驗證簽名

在接收端,必須在處理載荷之前驗證簽名。使用常數時間比較以防止時序攻擊。

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// 在你的 webhook 端點中:
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-signature'];
  const payload = req.rawBody; // 原始主體作為字串
  if (!verifySignature(payload, signature, 'your-secret-key')) {
    return res.status(401).send('Invalid signature');
  }
  // 處理事件
  res.status(200).send('OK');
});

注意: 始終使用 HTTPS 以防止中間人攻擊。此外,考慮在簽名載荷中包含時間戳以防止重放攻擊。

實作指數退避重試

即使接收方可靠,網路問題或暫時性中斷也可能導致 webhook 傳遞失敗。穩健的發送方應重試失敗的傳遞。然而,簡單的重試可能會淹沒接收方或導致重複處理。解決方案:帶有抖動和最大重試次數的指數退避。

重試策略

範例:Node.js 中的重試邏輯

async function deliverWebhook(url, payload, secret, maxRetries = 5) {
  const signature = signPayload(payload, secret);
  let attempt = 0;
  while (attempt < maxRetries) {
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Signature': signature },
        body: payload,
      });
      if (response.ok) return;
      throw new Error(`HTTP ${response.status}`);
    } catch (error) {
      attempt++;
      if (attempt >= maxRetries) throw error;
      const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

此函數最多重試 5 次,使用指數退避和抖動,延遲上限為 30 秒。根據你的需求調整參數。

確保冪等性

重試可能導致重複傳遞,因此你的 webhook 處理程序必須是冪等的。在載荷中包含唯一的事件 ID,並讓接收方追蹤已處理的 ID 以避免重複處理。

// 在接收方:
const processedEvents = new Set();

app.post('/webhook', (req, res) => {
  const { id } = req.body;
  if (processedEvents.has(id)) {
    return res.status(200).send('Already processed');
  }
  // 處理事件
  processedEvents.add(id);
  res.status(200).send('OK');
});

在生產環境中,使用持久化存儲(例如 Redis 或資料庫)來跨重啟追蹤已處理的 ID。

比較:簽名與未簽名 Webhooks

方面 未簽名 簽名 (HMAC)
安全性 任何人都可以偽造請求 只有密鑰持有者才能創建有效簽名
完整性 載荷可能被篡改 篡改會使簽名無效
複雜度 低 中等(密鑰管理、驗證)
使用案例 內部、可信網路 任何公開 webhook

常見問題

什麼是 webhook 簽名?

Webhook 簽名是載荷的加密雜湊(通常是 HMAC),使用共享密鑰創建。它允許接收方驗證請求來自預期的發送方,並且載荷未被更改。

我應該重試失敗的 webhook 多少次?

沒有固定次數,但常見做法是使用指數退避重試 3 到 5 次。之後,將傳遞標記為失敗並通知你的團隊。考慮事件的重要性:付款通知可能比非關鍵更新需要更多重試。

我可以在沒有 HTTPS 的情況下使用 webhooks 嗎?

在生產環境中,絕不應通過純 HTTP 使用 webhooks。HTTPS 在傳輸過程中加密載荷,防止竊聽和中間人攻擊。即使有簽名,HTTP 也會暴露敏感資料。

準備測試你的 webhook 載荷了嗎?使用我們的 JSON Formatter 在發送或除錯之前美化和驗證 JSON 載荷。