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

Backend2026-10-22TryQuickToolBox

サードパーティサービスからイベントを受け取るWebhookエンドポイントを構築したとします。テストではすべてうまくいきますが、本番環境では重複イベント、通知の欠落、偽造リクエストに関するセキュリティ上の懸念に気づくでしょう。これらは適切な保護策なしにWebhookを実装する際のよくある落とし穴です。

Webhookは、何かが起こったときにサービスがアプリケーションに通知するためのHTTPコールバックです。ポーリングとは異なり、リアルタイムの更新を提供します。しかし、信頼性高く使用するには、2つの重要な側面に対処する必要があります:真正性(リクエストが期待される送信者から来たことを確認する)と配信(失敗とリトライを処理する)です。この記事では、実用的なコード例を交えて両方を説明します。

Webhookの仕組み

Webhookはユーザー定義のHTTP POSTエンドポイントです。イベントが発生すると、ソースサービスはそのURLにイベントを記述したペイロードを含むリクエストを送信します。たとえば、決済処理サービスは課金が成功したときにWebhookを送信するかもしれません。

流れはシンプルです:

  1. サービスにWebhook URLを登録します。
  2. イベントが発生すると、サービスはそのURLにHTTP POSTを送信します。
  3. エンドポイントはペイロードを処理し、受信を確認する2xxステータスを返します。

適切な検証がなければ、誰でもエンドポイントにリクエストを送信できます。リトライ処理がなければ、一時的な障害でイベントが失われる可能性があります。それぞれに対処しましょう。

署名付きペイロードでWebhookを保護する

Webhookが期待される送信者から発信されたことを保証するため、サービスは共有シークレットを使用してペイロードに署名します。署名は通常、リクエストボディのHMAC(Hash-based Message Authentication Code)であり、X-Signatureのようなヘッダーで送信されます。

HMAC署名の仕組み

送信者は秘密鍵と生のリクエストボディを使用してHMACを計算し、16進数エンコードされた署名をヘッダーに含めます。エンドポイントは同じ秘密鍵でHMACを再計算し、受信した署名と比較します。一致すれば、リクエストは真正です。

Expressを使用したNode.jsの最小例を次に示します:

const crypto = require('crypto');
const express = require('express');
const app = express();

// 署名検証には生のボディを使用
app.use(express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-signature'];
  const secret = process.env.WEBHOOK_SECRET;
  const payload = req.body;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).send('Invalid signature');
  }

  // イベントを処理
  const event = JSON.parse(payload);
  // ... イベントを処理
  res.status(200).send('OK');
});

重要なポイント:

追加のセキュリティ対策

信頼性の高いリトライの実装

Webhookの配信は保証されません。ネットワーク障害、サーバーダウン、または非2xxステータスの返却が起こり得ます。これに対処するため、送信者は通常、指数バックオフでリトライします。受信者として、エンドポイントを冪等に設計し、迅速に応答する必要があります。

送信側の場合(Webhookを送信する場合)

Webhookを送信するサービスを構築している場合、バックオフ付きのリトライを実装してください。一般的な戦略:

Pythonでのリトライロジックの例:

import time
import requests
from requests.exceptions import RequestException

def send_webhook(url, payload, max_retries=5):
    for attempt in range(max_retries):
        try:
            response = requests.post(url, json=payload, timeout=5)
            if 200 <= response.status_code < 300:
                return True
            elif 400 <= response.status_code < 500 and response.status_code != 429:
                # クライアントエラー、リトライしない
                return False
        except RequestException:
            pass
        # ジッター付き指数バックオフ
        delay = (2 ** attempt) + random.uniform(0, 1)
        time.sleep(delay)
    return False

受信側の場合

エンドポイントは次のようにする必要があります:

データベースの一意制約を使用した冪等性の例:

CREATE TABLE processed_events (
  event_id VARCHAR(255) PRIMARY KEY,
  processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

処理前にイベントIDの挿入を試みます。主キー違反が発生した場合は処理をスキップします。

ローカルでのWebhookテスト

開発中、サービスがローカルサーバーに到達するための公開URLが必要です。ngrokやlocaltunnelなどのツールがlocalhostを公開します。開発中は常にテスト用シークレットを使用し、署名を検証して問題を早期に発見してください。

比較:ポーリング vs Webhook

側面 ポーリング Webhook
レイテンシ 間隔に依存 ほぼリアルタイム
サーバー負荷 高い(頻繁なリクエスト) 低い(イベント時のみ)
複雑さ 実装が簡単 署名検証とリトライ処理が必要
信頼性 リトライを自分で制御 送信者のリトライポリシーに依存

FAQ

署名付きWebhookペイロードとは何ですか?

署名付きペイロードには、共有シークレットで生成された暗号署名(通常はHMAC)が含まれます。これにより、受信者はリクエストが期待される送信者から来たものであり、改ざんされていないことを検証できます。

重複したWebhook配信を処理するにはどうすればよいですか?

ペイロードの一意のイベントIDを使用してエンドポイントを冪等にします。処理済みIDを保存し、重複を無視します。これにより、リトライやネットワーク問題が二重処理を引き起こさないようにします。

失敗したWebhook配信をリトライすべきですか?

はい、ただし一時的なエラー(ネットワーク問題、5xx)のみです。ジッター付き指数バックオフを使用し、最大リトライ回数を設定します。4xxエラー(429を除く)はリクエスト自体に問題があることを示すため、リトライを避けてください。

Webhookペイロードをデバッグする際、JSONデータを検査する必要がよくあります。JSON Formatterは、JSONペイロードを迅速に整形して検証するのに役立ちます。