Webhook解説:署名付きペイロードとリトライ
決済ゲートウェイを統合し、決済が成功したときにデータベースを更新する必要があるとします。数秒ごとにAPIをポーリングするのは無駄で遅いです。Webhookは、イベントが発生したときにサーバーにプッシュすることでこの問題を解決します。しかし、適切なセキュリティと信頼性がなければ、Webhookはバグや脆弱性の原因になり得ます。この記事では、署名付きペイロードとリトライに焦点を当て、Webhookを正しく実装する方法を説明します。
Webhookとは?
WebhookはHTTPコールバックです。サービスでイベントが発生すると、そのサービスが指定したURLにHTTP POSTリクエストを送信します。ペイロードには通常、イベントに関するJSONデータが含まれます。例えば、顧客が決済を完了すると、Stripeはcharge.succeededイベントをエンドポイントに送信します。
Webhookはプッシュベースであるため、ポーリングと比較してレイテンシとサーバー負荷を削減できます。しかし、課題もあります。リクエストが本物であることをどうやって確認するのか?イベント発生時にサーバーがダウンしていたらどうなるのか?
署名付きペイロードが重要な理由
誰でもWebhookエンドポイントにHTTP POSTを送信できます。検証がなければ、攻撃者がイベントを偽造し、注文を支払い済みとしてマークするなどの不正な操作を引き起こす可能性があります。署名付きペイロードは、送信者とあなただけが生成できる暗号署名を含めることでこれを解決します。
最も一般的な方法は、共有シークレットを使用したHMAC(Hash-based Message Authentication Code)です。送信者はシークレットを使用してペイロードのハッシュを計算し、ヘッダーに含めます。あなたはハッシュを再計算して比較します。一致すれば、リクエストは本物です。
署名付きペイロードの検証方法
Node.jsとExpressを使用した手順を以下に示します。
- 生のリクエストボディを文字列として取得します。検証前にJSONとしてパースしないでください。パースするとバイトシーケンスが変更される可能性があります。
- ヘッダーから署名を抽出します(例:
X-Signature)。 - シークレットと生のボディを使用してHMACを計算します。
- 計算した署名と受信した署名を、タイミング攻撃を防ぐための定数時間比較を使用して比較します。
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
res.status(200).send('OK');
});
常に強力なシークレットを使用し、定期的にローテーションしてください。クライアントサイドのコードに公開しないでください。
バックオフ付きリトライの実装
Webhookはインターネット経由で配信されるため、失敗は発生します。サーバーがダウンしているか、ネットワークに問題が生じる可能性があります。堅牢なWebhookシステムは、指数バックオフを使用して失敗した配信をリトライします。
通常、送信者はタイムアウト(例:5秒)以内に2xxレスポンスを受信しない場合にリトライします。リトライスケジュールは、即時、その後1分後、5分後、30分後、2時間後など、最大試行回数(例:5回)まで続く場合があります。
受信者として、タイムアウトを避けるために迅速に応答する必要があります。200 OKで受信を確認し、イベントを非同期で処理します。
冪等性の処理
リトライは重複配信を引き起こす可能性があるため、イベント処理は冪等でなければなりません。ペイロードに一意のイベントIDを含め、処理済みのIDを保存します。処理前に、IDが既に処理されているかどうかを確認します。
async function processEvent(event) {
const { id, type, data } = event;
if (await db.processedEvents.findOne({ id })) {
return; // Already processed
}
await db.processedEvents.insertOne({ id });
// Handle event based on type
}
データベーストランザクションを使用して、正常に処理された後にのみイベントが処理済みとしてマークされるようにします。
Webhookセキュリティのベストプラクティス
- 常に署名を検証する(処理前)。
- HTTPSを使用する(盗聴を防ぐため)。
- タイムスタンプを検証する(含まれている場合、リプレイ攻撃を防ぐため)。
- ペイロードサイズを制限する(サービス拒否を避けるため)。
- すべてのWebhook試行をログに記録する(デバッグと監査のため)。
比較:ポーリング vs Webhook
| 側面 | ポーリング | Webhook |
|---|---|---|
| レイテンシ | 高(間隔ベース) | 低(ほぼリアルタイム) |
| サーバー負荷 | 高(常時リクエスト) | 低(イベント時のみ) |
| 複雑さ | 実装が簡単 | セキュリティとリトライが必要 |
| 信頼性 | ポーリング頻度に依存 | リトライロジックに依存 |
FAQ
Webhookをローカルでテストするには?
ngrokなどのツールを使用して、ローカルサーバーをインターネットに公開します。Webhookプロバイダーを設定して、ngrok URLにイベントを送信します。
Webhook送信時にサーバーがダウンしていたら?
送信者はバックオフ付きでリトライする必要があります。サーバーが高可用性で、タイムアウトを避けるために迅速に応答することを確認してください。
HMACの代わりに非対称署名を使用できますか?
はい、一部のプロバイダーはRSAまたはECDSA署名を使用しています。公開鍵で検証します。HMACはよりシンプルですが、共有シークレットが必要です。
Webhookペイロードをデバッグする際には、JSONを読みやすくフォーマットすると便利です。私たちのJSON Formatterを試して、Webhookペイロードを即座に整形して検証してください。