Webhook解説:署名付きペイロードとリトライ
サードパーティの決済プロバイダーを統合したばかりだとします。ユーザーが購入を完了したのに、データベースが更新されない。なぜでしょうか?アプリに通知するはずのWebhookがサイレントに失敗したからです。適切な署名検証とリトライロジックがなければ、Webhookは不可解なバグやセキュリティホールの原因になり得ます。
この記事では、Webhookの仕組み、署名付きペイロードを検証する必要がある理由、そして堅牢なリトライメカニズムの設計方法を説明します。また、Webhookを効果的にテストおよびデバッグする方法も紹介します。
Webhookとは?
Webhookはユーザー定義のHTTPコールバックです。イベントが発生したときに、あるシステムが別のシステムへリアルタイムにデータを送信できるようにします。たとえば、決済が処理されると、決済プロバイダーは指定したURLにHTTP POSTリクエストを送信し、イベントの詳細を含めます。
アプリが繰り返し更新を要求するポーリングとは異なり、Webhookはデータが利用可能になるとすぐにプッシュします。これにより効率的でほぼリアルタイムになります。
セキュリティの問題:なぜ署名を検証するのか?
Webhookエンドポイントは公開されたURLです。誰でもPOSTリクエストを送信できます。検証がなければ、攻撃者は実際の支払いなしに注文を支払い済みにするなどのリクエストを偽造できます。これを防ぐため、プロバイダーは秘密鍵で各ペイロードに署名します。
HMAC署名の仕組み
ほとんどのプロバイダーはSHA-256を用いたHMAC(Hash-based Message Authentication Code)を使用します。共有秘密鍵を使ってリクエストボディのハッシュを計算し、X-Signatureなどのヘッダーに含めます。サーバーはハッシュを再計算し、ヘッダーと比較します。一致すれば、リクエストは本物です。
以下はPythonでの簡単な例です:
import hmac
import hashlib
def verify_signature(payload, secret, signature):
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
タイミング攻撃を避けるため、常にhmac.compare_digestのような定数時間比較関数を使用してください。
避けるべき一般的な間違い
- 間違った秘密鍵の使用:各環境(テスト、本番)には独自の秘密鍵が必要です。
- タイムスタンプの検証不足:一部のプロバイダーはリプレイ攻撃を防ぐためにタイムスタンプを含めます。数分以上古いリクエストは拒否してください。
- 秘密鍵のログ出力:本番環境では秘密鍵や完全な署名を絶対にログに記録しないでください。
信頼性の高いリトライの設計
Webhookはインターネット経由で送信されるため、失敗は避けられません。ネットワークが落ちたり、サーバーが再起動したり、エンドポイントがタイムアウトしたりします。優れたWebhookシステムは、指数バックオフを用いて失敗した配信をリトライします。
リトライの一般的な仕組み
プロバイダーは通常、1分後、5分後、30分後などと遅延を増やしながら、最大試行回数(例:5〜10回)までリトライします。すべてのリトライが失敗すると、イベントは失敗としてマークされ、手動での再送が可能になる場合があります。
エンドポイントは受信を確認するために、迅速に2xxステータスコードを返す必要があります。非2xxコードを返したりタイムアウトしたりすると、プロバイダーはリトライします。
冪等性の実装
リトライは重複配信を引き起こす可能性があるため、エンドポイントは冪等でなければなりません。ペイロードまたはヘッダーに含まれるイベントIDを使用して、処理済みイベントを追跡します。処理前に、そのIDをすでに処理したかどうかを確認します。処理済みIDはTTL付きでデータベースに保存します。
冪等性チェックの例:
def process_event(event_id, data):
if db.exists(f"processed:{event_id}"):
return # already processed
# process data
db.setex(f"processed:{event_id}", 86400, "1") # expire after 24h
Webhookレシーバーの構築:ステップバイステップ
- HTTPSエンドポイントを公開する:転送中のデータを保護するためにTLSを使用します。
- 署名を検証する:無効な署名のリクエストは401で拒否します。
- タイムスタンプを確認する:提供されている場合、許容範囲内(例:5分)であることを確認します。
- ペイロードを解析する:JSON構造と必須フィールドを検証します。
- 冪等性を確認する:イベントIDを使用して重複を避けます。
- 非同期で処理する:すぐに200を返し、タイムアウトを避けるためにバックグラウンドジョブで処理します。
- ログと監視:失敗を記録し、アラートを設定します。
ローカルでのWebhookテスト
開発中は、ローカルマシンでWebhookを受信する方法が必要です。ngrokやlocaltunnelなどのツールは、localhostに転送する公開URLを作成します。あるいは、多くのプロバイダーがテストイベントをトリガーするCLIを提供しています。
常に有効な署名と無効な署名の両方で署名検証をテストしてください。エラーコードを返してプロバイダーのリトライ動作を観察することで、リトライをシミュレートします。
比較:Webhook vs. ポーリング
| 側面 | Webhook | ポーリング |
|---|---|---|
| リアルタイム性 | ほぼリアルタイム | 間隔による遅延 |
| 効率性 | イベント発生時のみデータ送信 | データがなくても頻繁にリクエスト |
| 複雑さ | エンドポイント、署名検証、リトライが必要 | 実装がより簡単 |
| 信頼性 | リトライメカニズムに依存 | スケジュールを自分で制御 |
FAQ
署名付きWebhookペイロードとは何ですか?
署名付きペイロードには暗号署名(通常はHMAC)が含まれ、受信者はリクエストが期待される送信者から来たものであり、改ざんされていないことを検証できます。
Webhookは何回リトライすべきですか?
ほとんどのプロバイダーは指数バックオフで5〜10回リトライします。正確な回数はプロバイダーのポリシーによります。エンドポイントは重複を処理できるよう冪等であるべきです。
WebhookはどのHTTPステータスコードを返すべきですか?
受信成功を確認するために2xxステータスコード(例:200 OK)を返します。それ以外のコード(4xxまたは5xx)は失敗を示し、リトライをトリガーします。
Webhookペイロードをテストする準備はできましたか?JSON Formatterを使って、JSONペイロードをすばやく検証し整形しましょう。