Webhook解説:署名付きペイロードとリトライロジック

Backend2026-10-15TryQuickToolBox

決済ゲートウェイを統合したばかりだとします。ドキュメントには「決済が成功したらWebhookを送信します」と書いてあります。素晴らしい。しかし、次の疑問が浮かびます。リクエストが本当に相手から来たとどうやって確認するのか?送信時にサーバーがダウンしていたら?同じイベントを2回送ってきたら?

Webhookは強力ですが、落とし穴もあります。このガイドでは、正しく扱うために必要な仕組み、セキュリティ、信頼性のパターンを解説します。

Webhookとは何か?

Webhookとは、特定のイベントが発生したときに、あるシステムが別のシステムへ送信するHTTP POSTリクエストです。アプリが数秒ごとにAPIをポーリングする(「新しい注文は?新しい注文は?」)代わりに、プロバイダーが何かが起きた瞬間にデータをプッシュしてくれます。

典型的な流れ:

  1. プロバイダーにURL(例:https://yourapp.com/webhooks/payments)を登録します。
  2. イベントが発生します(決済成功、ユーザー登録、ファイルアップロード)。
  3. プロバイダーがイベントを記述したJSONボディを含むPOSTリクエストをあなたのURLに送信します。
  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');

  // タイミング攻撃を防ぐための定数時間比較
  return crypto.timingSafeEqual(
    Buffer.from(receivedSig, 'hex'),
    Buffer.from(expectedSig, 'hex')
  );
}

重要:署名検証には常に生のリクエストボディを使用してください。フレームワークがJSONをパースして再シリアライズすると、バイト列が異なり検証が失敗する可能性があります。パース前に生のボディを取得してください。

タイムスタンプの検証

タイムスタンプが最近のものか(例:5分以内)を確認してください。これにより、攻撃者が有効なリクエストをキャプチャして後で再送するリプレイ攻撃を防げます。

リトライロジック:障害を優雅に処理する

サーバーがダウンしていたり、過負荷だったり、ネットワーク障害が発生したりするかもしれません。プロバイダーは失敗した配信をリトライすることで対処します。しかし、エンドポイントはリトライを安全に処理できるよう設計する必要があります。

ジッター付き指数バックオフ

プロバイダーは通常、遅延を増やしながらリトライします:1分、5分、30分、2時間など。これは指数バックオフと呼ばれます。サンダリングハード問題を避けるためにランダムなジッターを加えるものもあります。

あなた側ではプロバイダーのリトライスケジュールを制御できませんが、次のことを行うべきです:

冪等性:安全なリトライの鍵

リトライとは、同じイベントを複数回受け取る可能性があるということです。処理は冪等でなければなりません。つまり、同じイベントを2回処理しても1回処理したのと同じ結果になる必要があります。

一般的なアプローチ:ペイロードに一意のイベント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' };
}

このパターンにより、プロバイダーがイベントを3回送信しても、あなたは1回だけ処理します。

比較:ポーリング vs Webhook

項目 ポーリング Webhook
レイテンシ 間隔に依存(例:30秒) ほぼリアルタイム
サーバー負荷 イベントがなくても一定 イベント発生時のみ
複雑さ 実装が簡単 エンドポイント、検証、リトライが必要
信頼性 リトライを自分で制御 プロバイダーのリトライポリシーに依存

リアルタイム更新にはWebhookが優れていますが、配信漏れが心配な重要なイベントにはポーリングをフォールバックとして使えます。

ベストプラクティスチェックリスト

Webhookの問題をデバッグする

Webhookが失敗したとき、最初のステップは生のリクエストを検査することです。よくある問題:

HTTPトラフィックをデバッグする場合、Nginx Log Analyzerのようなツールが、リクエスト失敗やレスポンスコードのパターンを見つけるのに役立ちます。

FAQ

Webhookシークレットとは何で、どう保存すればよいですか?

シークレットはペイロードの署名に使われる共有キーです。環境変数やシークレットマネージャーに保存し、コードやバージョン管理には絶対に含めないでください。プロバイダーが対応していれば定期的にローテーションしましょう。

プロバイダーは失敗したWebhookを何回リトライしますか?

プロバイダーによって異なります。指数バックオフで24時間リトライするものもあれば、3日間のものもあります。正確なスケジュールと最大試行回数はプロバイダーのドキュメントを確認してください。

サーバーレス関数でWebhookを使えますか?

はい、ただし実行時間制限に注意してください。Webhookを素早く確認(200を返す)し、重い処理をキューやバックグラウンド関数にオフロードしてタイムアウトを避けましょう。

堅牢なWebhook連携を構築する準備はできましたか?まず署名の検証と冪等性の実装から始めましょう。この2つのステップが何時間ものデバッグを節約し、コストのかかるセキュリティインシデントを防ぎます。