Webhook解説:署名付きペイロードとリトライ
決済ゲートウェイやCI/CDサービスを統合し、リアルタイムイベントを受信する必要が出てきました。Webhookは標準的なソリューションですが、2つの重要な課題があります。リクエストが本当に送信者から来たものであることを検証すること、そしてサーバーが一時的に利用できない場合でもイベントが失われないようにすることです。この記事では、署名付きペイロードとリトライメカニズムを実装して、堅牢なWebhookレシーバーを構築する方法を説明します。
Webhookとは何か?
WebhookはHTTPコールバックです。アプリケーションが更新のためにAPIをポーリングする代わりに、イベントが発生するたびにサービスが指定したURLにHTTP POSTリクエストを送信します。このプッシュモデルはレイテンシとサーバー負荷を軽減します。ただし、リクエストは外部サービスから発信されるため、信頼できない入力として扱う必要があります。
なぜペイロードに署名するのか?
検証がない場合、Webhookエンドポイントを発見した誰でも偽のイベントを送信でき、不正なアクションを引き起こす可能性があります。共有シークレットでペイロードに署名することで、送信者の身元を確認し、ペイロードが改ざんされていないことを保証できます。最も一般的な方法は、SHA-256を使用したHMAC(Hash-based Message Authentication Code)です。
HMAC署名の仕組み
送信者は、秘密鍵を使用して生のリクエストボディのHMACダイジェストを計算し、それをヘッダー(例:X-Signature)に含めます。サーバーは同じ秘密鍵でHMACを再計算し、受信した署名と比較します。一致すれば、リクエストは本物です。
// Node.jsの例:Webhook署名の検証
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const digest = hmac.digest('hex');
// タイミング攻撃を防ぐためにtimingSafeEqualを使用
return crypto.timingSafeEqual(
Buffer.from(digest, 'hex'),
Buffer.from(signature, 'hex')
);
}
重要:タイミングの違いを通じて情報が漏洩するのを避けるため、常に定数時間比較関数(timingSafeEqualなど)を使用してください。また、HMACは解析されたJSONではなく生のボディに対して計算してください。再シリアライズするとバイトシーケンスが変わる可能性があるためです。
リトライの実装
署名があっても、ネットワークの問題やサーバーエラーによりWebhook配信が失敗することがあります。信頼性の高い送信者は、通常指数バックオフを使用して失敗した配信をリトライします。受信者として、エンドポイントを冪等に設計し、適切なステータスコードを返す必要があります。
送信者側のリトライ戦略
- 5xxおよびネットワークエラーでリトライ:これらは一時的な問題を示します。
- 4xxではリトライしない:クライアントエラー(例:400 Bad Request)は通常永続的であり、リトライしても役に立ちません。
- 指数バックオフを使用:受信者を圧倒しないように、各リトライ間の待機時間を長くします。
- 最大リトライ回数を設定:制限後に配信を失敗としてマークし、アラートを出します。
冪等なレシーバーの設計
リトライにより重複配信が発生する可能性があるため、レシーバーは同じイベントを複数回処理しても悪影響がないようにする必要があります。ペイロード(またはヘッダー)に一意のイベントIDを含め、処理済みIDを保存します。イベントIDが既に処理されている場合は、再処理せずにすぐに200 OKを返します。
// 冪等なWebhook処理の擬似コード
function handleWebhook(event) {
if (processedEvents.has(event.id)) {
return { status: 200, body: 'Already processed' };
}
// イベントを処理...
processedEvents.add(event.id);
return { status: 200, body: 'Success' };
}
Webhookのセキュリティと信頼性のベストプラクティス
- HTTPSを使用:盗聴を防ぐために、常にTLS経由でWebhookエンドポイントを提供します。
- タイムスタンプを検証:署名にタイムスタンプが含まれている場合、数分より古いリクエストを拒否してリプレイ攻撃を防ぎます。
- 迅速に応答:重い処理を行う前に、2xxステータスで受信を確認します。作業を非同期にキューに入れます。
- すべてをログに記録:受信したWebhookのログ(ヘッダーとペイロードを含む、機密データは編集)を保持して問題をデバッグします。
- シークレットを定期的にローテーション:複数のアクティブなシークレットをサポートして、シームレスなローテーションを可能にします。
比較:ポーリング vs. Webhook
| 側面 | ポーリング | Webhook |
|---|---|---|
| レイテンシ | 間隔に依存 | ほぼリアルタイム |
| サーバー負荷 | 高い(頻繁なリクエスト) | 低い(イベント時のみ) |
| 複雑さ | 実装が簡単 | 署名検証とリトライ処理が必要 |
| 信頼性 | リトライを自分で制御 | 送信者のリトライポリシーに依存 |
FAQ
Webhook署名を検証するには?
共有シークレットを使用して生のリクエストボディのHMACを計算し、定数時間比較関数を使用してヘッダーの署名と比較します。
Webhookエンドポイントは何を返すべきですか?
イベントを受信したらすぐに2xxステータスコード(例:200 OK)を返します。4xxまたは5xxを返すと、送信者がリトライしたり、配信を失敗としてマークしたりする可能性があります。
署名なしでWebhookを使用できますか?
推奨されません。署名がないと送信者を検証できず、エンドポイントが偽のリクエストに対して脆弱になります。本番環境では常に署名を使用してください。
Webhookペイロードをデバッグする際、JSONのフォーマットは不可欠です。受信したJSONデータをすばやく整形して検証するには、JSONフォーマッターをご利用ください。