Webhook解説:署名付きペイロードとリトライ戦略
APIを構築し、イベントが発生したときに他のサービスに通知する必要が出てきました。ポーリングは非効率なので、Webhookを実装します。しかしすぐに2つの一般的な問題に直面します。ペイロードが改ざんされていないことをどう保証するか、そして受信エンドポイントがダウンしているときにどうするかです。この記事では、署名付きペイロードとリトライ戦略を使用してWebhookを安全で信頼性の高いものにする方法を説明します。
Webhookに署名とリトライが必要な理由
WebhookはイベントによってトリガーされるHTTPコールバックです。実装は簡単ですが、セキュリティと信頼性の課題があります。署名がないと、攻撃者がエンドポイントへのリクエストを偽造し、データ破損や不正な操作を引き起こす可能性があります。リトライがないと、一時的な障害(ネットワークの瞬断や一時的なサーバーエラーなど)によってイベントが失われます。
署名付きペイロードは真正性を解決します。送信者が秘密鍵でペイロードに署名し、受信者が検証します。リトライは信頼性を解決します。送信者はバックオフを伴って失敗した配信を再試行します。
署名付きペイロードの仕組み
署名付きペイロードには、リクエストボディと共有秘密鍵から生成された暗号署名が含まれます。最も一般的な方法は、SHA-256を使用したHMAC(Hash-based Message Authentication Code)です。
署名の生成
送信者は秘密鍵を使用して生のリクエストボディのHMACを計算します。署名はヘッダーで送信され、多くの場合、リプレイ攻撃を防ぐためにタイムスタンプも一緒に送信されます。
// Node.jsの例:HMAC署名の生成
const crypto = require('crypto');
function signPayload(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return { timestamp, signature };
}
タイムスタンプと署名をヘッダーに含めます。例:X-Webhook-Timestamp と X-Webhook-Signature。
署名の検証
受信者は、受信したタイムスタンプと生のボディを使用して署名付きペイロードを再構築し、HMACを計算して安全に比較する必要があります。
// Node.jsの例:署名の検証
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// タイミングセーフな比較を使用
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
タイミング攻撃を防ぐために、常にタイミングセーフな比較を使用してください。また、リプレイ攻撃を軽減するために、タイムスタンプが最近のもの(例:5分以内)であることを確認してください。
リトライ戦略の設計
リトライは信頼性の高いWebhook配信に不可欠です。しかし、単純なリトライは受信者に過負荷をかけたり、重複処理を引き起こしたりする可能性があります。優れたリトライ戦略には以下が含まれます:
- 指数バックオフ: 各リトライ間の待機時間を長くします(例:1秒、2秒、4秒、8秒...)。
- 最大リトライ回数: 無限ループを避けるために試行回数を制限します(例:5回)。
- デッドレターキュー: 最大リトライ後、手動検査のためにイベントを保存します。
- 冪等性: 受信者が重複を排除できるように一意のイベントIDを含めます。
以下は擬似コードでの簡単なリトライループです:
function deliverWithRetry(url, payload, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, { method: 'POST', body: payload });
if (response.ok) return;
} catch (error) {
// エラーをログに記録
}
attempt++;
const delay = Math.pow(2, attempt) * 1000; // 指数バックオフ
await new Promise(resolve => setTimeout(resolve, delay));
}
// デッドレターキューに移動
}
サンダリングハード問題を避けるために、バックオフにジッターを追加することを検討してください。
比較:ポーリング vs リトライ付きWebhook
| 側面 | ポーリング | リトライ付きWebhook |
|---|---|---|
| レイテンシ | 高い(間隔に依存) | 低い(ほぼリアルタイム) |
| サーバー負荷 | 一定(イベントがない場合も) | イベント駆動型 |
| 信頼性 | 高い(受信者が制御) | リトライ戦略に依存 |
| セキュリティ | 標準的なAPI認証 | 署名付きペイロードが必要 |
ステップバイステップ:署名とリトライを備えたWebhookの実装
- 秘密鍵を生成 各Webhookエンドポイント用に生成し、安全に共有します。
- イベント発生時、ペイロード(JSON)を構築し、秘密鍵とタイムスタンプを使用してHMAC-SHA256で署名を計算します。
- POSTリクエストを送信 エンドポイントに、ヘッダー:
Content-Type: application/json、X-Webhook-Timestamp、X-Webhook-Signatureを付けて送信します。 - 受信者側では、生のボディとタイムスタンプを使用して署名を検証します。無効またはタイムスタンプが古すぎる場合は拒否します。
- 配信が失敗した場合(非2xx応答またはネットワークエラー)、最大試行回数まで指数バックオフでリトライします。
- 最大リトライ後、失敗をログに記録し、オプションでアラートを送信します。手動リプレイのためにイベントをデッドレターキューに保存します。
- 冪等性を確保 受信者側でイベントIDを使用して重複を無視します。
よくある落とし穴とベストプラクティス
- 生のボディを使用しない: 多くのフレームワークはJSONを解析してボディを変更します。署名検証には生のボディを使用してください。
- タイムスタンプを無視する: タイムスタンプ検証がないと、リプレイ攻撃が可能になります。
- 冪等性がない: リトライは重複処理を引き起こす可能性があります。冪等性キーを使用してください。
- 無限リトライ: 常に最大リトライ制限とデッドレターキューを設定してください。
- ログを取らない: デバッグのために配信試行と失敗をログに記録してください。
FAQ
署名付きWebhookペイロードとは何ですか?
署名付きWebhookペイロードには、受信者がリクエストが予想される送信者から発信され、改ざんされていないことを検証できる暗号署名(例:HMAC)が含まれています。
失敗したWebhookを何回リトライすべきですか?
一般的な方法は、指数バックオフを伴う3〜5回のリトライです。正確な回数はSLAとイベントの重要度によって異なります。リトライ制限を超えたイベントには常にデッドレターキューを使用してください。
署名なしでWebhookを使用できますか?
推奨されません。署名がないと、エンドポイントは偽造リクエストに対して脆弱になります。リプレイ攻撃を防ぐために、常に署名(HMACなど)を使用し、タイムスタンプを検証してください。
Webhook配信をデバッグする際には、サーバーログの検査が重要です。Nginxを使用している場合、Nginx Log Analyzer がアクセスログを解析および分析してWebhookリクエストを追跡し、失敗を特定するのに役立ちます。