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

Backend2026-11-02TryQuickToolBox

Webhookに署名とリトライが必要な理由

決済プロバイダーやSaaSプラットフォームからWebhookを受信するエンドポイントを構築したとします。テストではすべてうまくいきますが、本番環境では2つの問題に直面します。リクエストが本当にプロバイダーから来たものかをどう確認するか、そしてサーバーがダウンしたり遅くなったりしたときに何が起こるかです。署名付きペイロードがなければ、誰でもリクエストを偽造できます。リトライがなければ、一度のネットワークの瞬断でイベントが永久に失われる可能性があります。

このガイドでは、両方を正しく実装する方法を説明します。HMAC署名、検証手順、リトライ戦略、そして冪等性—信頼性の高いWebhook処理の3つの柱をカバーします。

Webhook署名の仕組み

Webhook署名は、共有秘密鍵を知っている誰かがペイロードを作成したことを証明する暗号学的ハッシュです。ほとんどのプロバイダーはHMAC-SHA256を使用します。送信者は生のリクエストボディ(場合によってはタイムスタンプ付き)に対して署名を計算し、ヘッダーに含めます。受信者はそれを再計算して比較します。

典型的な流れは次のとおりです:

  1. プロバイダーとコンシューマーが秘密鍵を共有します(プロバイダーのダッシュボードで設定)。
  2. プロバイダーがペイロードを作成し、HMAC-SHA256(secret, payload)を計算して、X-Signatureのようなヘッダーで送信します。
  3. コンシューマーはリクエストを受信し、生のボディを読み取り、HMACを再計算し、定数時間関数を使用して比較します。
  4. 一致すればイベントを処理し、そうでなければ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)

    # Process the event
    event = request.json
    # ... handle event ...
    return '', 200

重要なポイント:

リトライ戦略の設計

署名があってもWebhookは失敗する可能性があります。サーバーがダウンしている、500を返す、またはタイムアウトするかもしれません。プロバイダーは通常、指数バックオフでリトライします。コンシューマーとしても、リトライを適切に処理する必要があります。

考慮すべき点:

ほとんどのプロバイダーは非2xx応答またはタイムアウトでリトライし、多くの場合最大24時間、間隔を増やしながら行います。詳細はプロバイダーのドキュメントを確認してください。

例:冪等なイベントハンドラー

データベーステーブルを使用して処理済みイベントIDを追跡します。簡略化したパターンは次のとおりです:

def handle_event(event_id, payload):
    if db.exists('processed_events', event_id):
        return  # already handled
    # process payload
    db.insert('processed_events', event_id)
    # ... actual work ...

高スループットの場合は、イベントIDに一意制約を使用し、重複キーエラーをキャッチします。

比較:署名付き vs 署名なしWebhook

側面署名付きペイロード署名なしペイロード
セキュリティ送信者の真正性を検証誰でもリクエストを偽造可能
リプレイ保護タイムスタンプで可能なし
実装の手間中程度(HMAC + タイムスタンプチェック)低い
推奨用途すべての本番Webhook内部テストのみ

一般的な落とし穴とベストプラクティス

FAQ

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

署名付きペイロードには暗号学的署名(通常はHMAC)が含まれており、受信者がリクエストが信頼できるソースから来たもので改ざんされていないことを検証できます。

アクションを重複させずにWebhookリトライを処理するにはどうすればよいですか?

イベントIDを追跡してハンドラーを冪等にします。処理前にIDが既に処理されているか確認し、そうであれば作業を繰り返さずに成功を返します。

署名なしでWebhookを使用できますか?

本番環境では推奨されません。署名がないと、正当なリクエストと偽造されたリクエストを区別できません。利用可能な場合は常に署名を検証してください。

Webhookペイロードのデバッグやサーバーログの分析には、JSON FormatterがJSON構造を迅速に検査および検証するのに役立ちます。