Webhook解説:署名付きペイロードとリトライ
Webhookが厄介な理由
Webhookはシンプルなアイデアです。APIをポーリングして変更を確認する代わりに、プロバイダーがHTTP POSTでイベントをエンドポイントにプッシュします。しかし実際には、Webhookは多くの開発者を悩ませる一連の課題をもたらします。リクエストが本当にプロバイダーから来たのかを検証すること、重複配信の処理、ネットワーク障害への対処などです。これらを無視すると、セキュリティ侵害、データ破損、ユーザーの怒りを招くリスクがあります。
この記事では、本番環境レベルのWebhookの最も重要な2つの側面、署名付きペイロードとリトライについて説明します。署名の検証方法、指数バックオフを用いたリトライの実装方法、そして処理を冪等にする方法をカバーします。
署名付きペイロード:真正性の証明
誰でもあなたのエンドポイントにHTTP POSTを送信できます。リクエストがプロバイダーから来たことを確認するために、プロバイダーは秘密鍵でペイロードに署名し、その署名をヘッダーで送信します。あなたの仕事は、署名を再計算して比較することです。
ほとんどのプロバイダーはHMAC-SHA256を使用します。署名は通常、生のリクエストボディ(場合によってはタイムスタンプ付き)に対して計算され、16進数でエンコードされます。例えば、Stripeはタイムスタンプと署名を含むStripe-Signatureのようなヘッダーを使用します。
HMACの仕組み
HMAC(Hash-based Message Authentication Code)は、秘密鍵とハッシュ関数を組み合わせます。同じ鍵とメッセージが与えられれば、出力は決定論的です。鍵がなければ、攻撃者は有効な署名を偽造できません。
以下はNode.jsでの最小限の例です:
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Pythonでも同様に簡単です:
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
署名検証のベストプラクティス
- 定数時間比較を使用する:タイミング攻撃を防ぐため。
crypto.timingSafeEqualとhmac.compare_digestはどちらも安全です。 - 生のボディを検証する:パースされたJSONオブジェクトではなく。パースと再シリアライズは空白を変更し、署名を破壊する可能性があります。
- タイムスタンプを含める:署名データに含めることでリプレイ攻撃を防ぎます。数分以上古いリクエストは拒否します。
- 秘密鍵を安全に保管する:環境変数やシークレットマネージャーを使用し、決してハードコードしないでください。
タイムスタンプとリプレイ攻撃への対処
多くのプロバイダーは署名ヘッダーにタイムスタンプを含めます。例えば、ヘッダーはt=1614556800,v1=abc123...のようになります。次のようにします:
- タイムスタンプと署名を抽出します。
- タイムスタンプが許容範囲内(例:5分)であることを確認します。
- タイムスタンプとペイロードを使用して署名を再計算します。
- 署名を安全に比較します。
これにより、攻撃者が古い有効なリクエストをリプレイするのを防ぎます。
リトライ:障害を優雅に処理する
Webhookはパブリックインターネット経由で配信されるため、障害は避けられません。エンドポイントがダウンしていたり、遅かったり、500エラーを返したりする可能性があります。堅牢なプロバイダーは失敗した配信をリトライしますが、あなたがプロバイダーである場合は、あなたの側でもリトライを処理する必要があります。
ジッター付き指数バックオフ
リトライする際は、即時のリトライでエンドポイントを叩き続けるのを避けます。指数バックオフを使用します:各失敗後に待機時間を長くします。同時に多くのWebhookが失敗したときのサンダリングハード問題を避けるために、ジッター(ランダム性)を追加します。
典型的なリトライスケジュールは次のようになります:
| 試行回数 | 遅延(約) |
|---|---|
| 1 | 即時 |
| 2 | 30秒 |
| 3 | 5分 |
| 4 | 30分 |
| 5 | 2時間 |
最大試行回数に達したら、配信を失敗としてマークし、アラートを出します。
冪等性:安全なリトライの鍵
リトライは重複配信を引き起こす可能性があるため、Webhookハンドラーは冪等でなければなりません。つまり、同じイベントを複数回処理しても、1回処理した場合と同じ効果が得られる必要があります。
冪等性を実現する方法:
- ペイロードに含まれる一意のイベントIDを使用する(例:
idフィールド)。処理済みIDを一意制約付きでデータベースに保存します。 - 操作を冪等に設計する:例えば、挿入の代わりにアップサートを使用する、またはリソースを作成する前に既に存在するか確認します。
- 迅速に2xxを返す:処理が重い場合は非同期で処理します。これにより、リトライをトリガーするタイムアウトの可能性を減らします。
プロバイダー側でのリトライ実装
Webhookシステムを構築する場合、信頼性の高い配信メカニズムが必要です。以下は簡略化されたアプローチです:
- イベントが発生したら、ペイロードとターゲットURLを含む配信ジョブをキューに入れます。
- ワーカーがジョブを取得し、タイムアウト(例:10秒)付きでHTTP POSTを送信します。
- レスポンスステータスが2xxでない場合、指数バックオフでリトライをスケジュールします。
- 試行回数を追跡し、最大回数(例:5回)後に諦めます。
- 失敗をログに記録し、ユーザーが手動でリプレイできる方法を提供します。
永続キュー(例:Redis、RabbitMQ、またはデータベーステーブル)を使用して、再起動後もリトライが生き残るようにします。
まとめ:Webhookレシーバーのチェックリスト
Webhookレシーバーを構築する際は、次のチェックリストに従ってください:
- 署名を検証する:プロバイダーの秘密鍵と定数時間比較を使用します。
- タイムスタンプを確認する:リプレイ攻撃を防ぎます。
- 迅速に2xxを返す:重い処理を行う前に受信を確認します。
- 必要に応じて非同期で処理する:キューを使用します。
- 冪等性を確保する:イベントIDを追跡します。
- すべてをログに記録する:デバッグと監査のため。
- 失敗を監視する:繰り返しのリトライに対してアラートを設定します。
FAQ
署名付きWebhookペイロードとは何ですか?
署名付きペイロードには、共有秘密鍵で生成された暗号署名(通常はHMAC-SHA256)が含まれます。受信者は署名を再計算して、リクエストが期待される送信者から来たものであり、改ざんされていないことを検証します。
Webhookにリトライが必要なのはなぜですか?
ネットワークの問題、サーバーのダウンタイム、一時的なエラーにより、Webhook配信が失敗する可能性があります。指数バックオフ付きのリトライにより、受信者に過負荷をかけずにイベントが最終的に配信されることが保証されます。
Webhookの重複処理を防ぐにはどうすればよいですか?
冪等性を使用します:ペイロードに一意のイベントIDを含め、処理済みIDを保存し、操作を冪等に設計します(例:アップサート)。これにより、Webhookが複数回配信されても効果は同じになります。
Webhookペイロードを検査またはデバッグする必要がある場合、JSON FormatterがJSONデータを迅速に整形して検証するのに役立ちます。