Webhook解説:署名付きペイロードとリトライ
Webhookに署名とリトライが必要な理由
決済プロバイダーやSaaSプラットフォームからWebhookを受信するエンドポイントを構築したとします。テストではすべてうまくいきますが、本番環境では2つの問題に直面します。リクエストが本当にプロバイダーから来たものかをどう確認するか、そしてサーバーがダウンしたり遅くなったりしたときに何が起こるかです。署名付きペイロードがなければ、誰でもリクエストを偽造できます。リトライがなければ、一度のネットワークの瞬断でイベントが永久に失われる可能性があります。
このガイドでは、両方を正しく実装する方法を説明します。HMAC署名、検証手順、リトライ戦略、そして冪等性—信頼性の高いWebhook処理の3つの柱をカバーします。
Webhook署名の仕組み
Webhook署名は、共有秘密鍵を知っている誰かがペイロードを作成したことを証明する暗号学的ハッシュです。ほとんどのプロバイダーはHMAC-SHA256を使用します。送信者は生のリクエストボディ(場合によってはタイムスタンプ付き)に対して署名を計算し、ヘッダーに含めます。受信者はそれを再計算して比較します。
典型的な流れは次のとおりです:
- プロバイダーとコンシューマーが秘密鍵を共有します(プロバイダーのダッシュボードで設定)。
- プロバイダーがペイロードを作成し、
HMAC-SHA256(secret, payload)を計算して、X-Signatureのようなヘッダーで送信します。 - コンシューマーはリクエストを受信し、生のボディを読み取り、HMACを再計算し、定数時間関数を使用して比較します。
- 一致すればイベントを処理し、そうでなければ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
重要なポイント:
- タイミング攻撃を避けるため、定数時間比較には
hmac.compare_digest(または同等のもの)を使用します。 - パース前に
request.get_data()を介して生のボディにアクセスします。 - すぐに200を返し、重い処理は非同期で行います。
リトライ戦略の設計
署名があってもWebhookは失敗する可能性があります。サーバーがダウンしている、500を返す、またはタイムアウトするかもしれません。プロバイダーは通常、指数バックオフでリトライします。コンシューマーとしても、リトライを適切に処理する必要があります。
考慮すべき点:
- 冪等性:各イベントには一意のIDが必要です。処理済みIDを保存して、リトライ時に重複処理を避けます。
- 迅速な応答:数秒以内に2xxステータスで受信を確認します。イベントをバックグラウンド処理用にキューに入れます。
- あなた側のリトライロジック:ダウンストリームサービスを呼び出す場合、バックオフとサーキットブレーカーを備えたリトライを実装します。
ほとんどのプロバイダーは非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 | 内部テストのみ |
一般的な落とし穴とベストプラクティス
- 検証前にパースしない:常に生のボディで最初に署名を検証します。
- HTTPSを使用する:署名は完全性を保護しますが、HTTPSは機密性を保護します。
- 秘密鍵をローテーションする:ローテーション中は複数のアクティブな秘密鍵をサポートしてダウンタイムを避けます。
- 失敗をログに記録する:デバッグのために署名の不一致とリトライを記録します。
- 実際のペイロードでテストする:webhook.siteやプロバイダーのCLIなどのツールを使用してイベントをシミュレートします。
FAQ
署名付きWebhookペイロードとは何ですか?
署名付きペイロードには暗号学的署名(通常はHMAC)が含まれており、受信者がリクエストが信頼できるソースから来たもので改ざんされていないことを検証できます。
アクションを重複させずにWebhookリトライを処理するにはどうすればよいですか?
イベントIDを追跡してハンドラーを冪等にします。処理前にIDが既に処理されているか確認し、そうであれば作業を繰り返さずに成功を返します。
署名なしでWebhookを使用できますか?
本番環境では推奨されません。署名がないと、正当なリクエストと偽造されたリクエストを区別できません。利用可能な場合は常に署名を検証してください。
Webhookペイロードのデバッグやサーバーログの分析には、JSON FormatterがJSON構造を迅速に検査および検証するのに役立ちます。