Webhooks解説:署名付きペイロードとリトライ
Webhookを使って決済ゲートウェイを連携したとします。テストではすべてうまくいきますが、本番環境ではイベントが時々失われたり、重複した通知を受け取って二重請求が発生したりします。根本原因は多くの場合、署名付きペイロードとリトライの処理方法にあります。この記事では、Webhookの仕組みを説明し、堅牢で安全な連携を構築するための実践的な手順を紹介します。
Webhookとは?
Webhookはユーザー定義のHTTPコールバックです。ソースシステムでイベントが発生すると(例:決済が処理される)、指定したURLにイベントデータを含むHTTP POSTリクエストが送信されます。ポーリングとは異なり、Webhookはほぼリアルタイムでデータをプッシュするため、レイテンシとサーバー負荷を軽減できます。
ただし、Webhookには課題があります。真正性の検証、障害の処理、そして正確に一度だけの処理を保証することです。それぞれについて見ていきましょう。
署名付きペイロード:真正性の検証
Webhookエンドポイントは公開されているため、誰でも偽のリクエストを送信できます。これを防ぐために、プロバイダーは秘密鍵でペイロードに署名します。署名を検証することで、リクエストが本物であることを確認できます。
HMAC署名の仕組み
ほとんどのプロバイダーはSHA-256を使用したHMAC(Hash-based Message Authentication Code)を使います。プロバイダーはリクエストボディと共有シークレットを使って署名を計算し、ヘッダー(例:X-Signature)に含めます。あなたは自身の側で署名を再計算して比較します。
Pythonでの例:
import hmac
import hashlib
def verify_signature(payload, secret, received_signature):
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received_signature)
重要:生のリクエストボディを使用してください。パースされたJSONは使用しないでください。パースすると空白が変更され、署名が壊れる可能性があります。
よくある落とし穴
- パース済みデータの使用:常に生のバイト列に対して検証してください。
- タイミング攻撃:定数時間比較(例:
hmac.compare_digest)を使用してください。 - タイムスタンプの無視:一部のプロバイダーはリプレイ攻撃を防ぐために署名にタイムスタンプを含めます。許容範囲内(例:5分)であることを検証してください。
リトライ:障害を適切に処理する
エンドポイントが一時的にダウンしていたり、ネットワークの問題で失敗したりする可能性があります。信頼性の高いWebhookシステムは、指数バックオフを使って失敗した配信をリトライします。
リトライ戦略
プロバイダーは通常、非2xxレスポンスまたはタイムアウト時にリトライします。一般的なパターン:
| 戦略 | 説明 | 例 |
|---|---|---|
| 固定間隔 | N秒ごとにリトライ | 30秒ごと、最大5回 |
| 指数バックオフ | 試行ごとに遅延を倍にする | 1s, 2s, 4s, 8s... |
| ジッター付き指数バックオフ | サンダリングハードを避けるためにランダム性を追加 | 1s ± 0.5s, 2s ± 1s... |
受信側として、送信側のリトライポリシーを制御することはできませんが、エンドポイントを耐障害性のある設計にすることはできます。
受信側のベストプラクティス
- 素早く応答する:数秒以内に2xxを返します。処理はバックグラウンドジョブにオフロードします。
- 冪等にする:ペイロードの冪等キーを使って重複処理を避けます。
- すべてをログに記録する:デバッグとリプレイのために受信したWebhookを保存します。
- 失敗を監視する:繰り返しの失敗に対するアラートを設定します。
冪等性の実装
重複は発生します。レスポンスが遅かったために送信側がリトライしたり、同じイベントを誤って二度処理したりすることがあります。冪等性により、イベントを複数回処理しても1回処理した場合と同じ結果になります。
一意のイベントID(多くの場合、ペイロードまたはヘッダーで提供される)を使用し、一意制約付きでデータベースに保存します。処理前にIDが存在するか確認し、存在すればスキップします。
def process_event(event_id, data):
if EventLog.exists(event_id):
return # already processed
EventLog.create(event_id)
# process data...
高スループットのシステムでは、競合状態を避けるために分散ロックまたはデータベーストランザクションを使用してください。
セキュリティに関する考慮事項
署名検証に加えて、以下を検討してください:
- HTTPS:盗聴を防ぐために常にTLSを使用してください。
- IP許可リスト:プロバイダーがIP範囲を公開している場合、受信リクエストを制限してください。
- レート制限:エンドポイントを悪用から保護してください。
- ペイロード検証:署名検証後も、インジェクション攻撃を避けるためにデータスキーマを検証してください。
ローカルでのWebhookテスト
開発中は、Webhookを受信するために公開URLが必要です。ngrokやlocaltunnelなどのツールがローカルサーバーを公開します。あるいは、多くのプロバイダーがlocalhostにイベントを転送するCLIを提供しています。
リトライ処理をテストするために失敗をシミュレートします。意図的に500エラーを返し、プロバイダーのリトライパターンを観察してください。
FAQ
Webhook署名を検証するにはどうすればよいですか?
共有シークレットを使用したHMACを使います。生のリクエストボディのハッシュを計算し、定数時間比較関数を使って署名ヘッダーと比較します。
重複したWebhookを受信した場合はどうすればよいですか?
イベントIDを保存して冪等性を実装します。ビジネスロジックを実行する前に、イベントがすでに処理されているか確認してください。
信頼性のためにWebhookのリトライだけに頼ることはできますか?
いいえ。リトライは役立ちますが、配信を保証することはできません。重要なイベントには、Webhookとポーリングのフォールバック、またはイベントを永続化するメッセージキューを組み合わせてください。
Webhookペイロードをデバッグする際には、JSONデータを検査する必要があるかもしれません。私たちのJSON Formatterを使って、ペイロードを素早く整形して検証してください。