Webhook解説:署名付きペイロードとリトライ
決済ゲートウェイを統合したとします。ドキュメントには「決済が成功したら、あなたのエンドポイントにPOSTリクエストを送信します」と書いてあります。簡単ですよね?JSONを受け取ってデータベースを更新し、200を返すシンプルなルートを作ります。しかし、次のようなことが起こります:顧客の決済は成功したのに、あなたのサーバーが一時的にダウンしていて、Webhookが届かない。あるいはもっと悪いことに、二重に届いてアカウントを二重にクレジットしてしまう。Webhookは失敗するまでシンプルに見えるものです。
この記事では、Webhookが実際にどのように動作するのか、なぜ署名付きペイロードがセキュリティに不可欠なのか、そして問題が発生しても統合の信頼性を保つリトライロジックの構築方法を説明します。
Webhookとは何か?
WebhookはHTTPコールバックです:アプリケーションが変更をポーリングする代わりに、イベントが発生したときにプロバイダーが指定したURLにHTTP POSTを送信します。プルではなくプッシュです。
一般的な例としては、決済確認(Stripe、PayPal)、CI/CDビルド通知(GitHub、GitLab)、メッセージングプラットフォーム(Slack、Discord)などがあります。パターンはシンプルです:URLを登録し、プロバイダーがそのURLにJSONペイロードを送信し、サーバーがそれらを処理します。
しかし、このシンプルさには2つの重要な課題が隠されています:真正性(リクエストが本当にプロバイダーから来たものかどうかをどうやって知るのか?)と信頼性(エンドポイントがダウンしているか遅い場合にどうなるのか?)です。
署名付きペイロードが重要な理由
Webhookエンドポイントは公開URLです。誰でもPOSTリクエストを送信できます。検証がなければ、攻撃者は決済成功イベントを偽造したり、出荷をトリガーしたり、データを破壊したりできます。
署名付きペイロードは、リクエストヘッダーに暗号署名を含めることでこれを解決します。プロバイダーは共有シークレットとリクエストボディを使用してHMAC(ハッシュベースのメッセージ認証コード)を計算し、ペイロードと一緒に送信します。サーバーはHMACを再計算して比較します。一致すれば、リクエストは真正です。
HMAC署名の仕組み
- プロバイダーとあなたは共有シークレットキーを共有します(セットアップ時に提供されます)。
- 各Webhookについて、プロバイダーはタイムスタンプと生のリクエストボディを連結し、シークレットを使用してHMAC-SHA256ハッシュを計算します。
- 署名はヘッダーに送信されます。多くの場合、
X-SignatureやStripe-Signatureのような名前です。 - サーバーは同じシークレットを使用してHMACを再計算し、受信した署名と比較します。
- 一致すればイベントを処理し、そうでなければ401で拒否します。
HMAC計算には常に生のリクエストボディを使用してください。検証前にフレームワークがJSONを解析すると、ボディが変更される可能性があり(空白、キーの順序)、検証が失敗する原因になります。
例:Node.jsでのWebhook署名の検証
const crypto = require('crypto');
const express = require('express');
const app = express();
// 署名検証には生のボディを使用
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// イベントを処理
const event = JSON.parse(payload.toString());
// ... イベントを処理
res.status(200).send('OK');
});
タイミング攻撃を防ぐためにtimingSafeEqualを使用していることに注意してください。署名の比較に単純な===を使わないでください。
リトライロジックの設計
署名があっても、Webhookは失敗する可能性があります。サーバーがダウンしている、ネットワークがリクエストをドロップする、処理がタイムアウトするなどです。プロバイダーは通常、失敗した配信をリトライしますが、システム側でもリトライを適切に処理するように設計する必要があります。
何が失敗と見なされるか?
ほとんどのプロバイダーは2xx以外のレスポンスを失敗と見なします。これには3xxリダイレクト、4xxクライアントエラー、5xxサーバーエラーが含まれます。タイムアウト(例:5秒)を強制するものもあります。迅速に応答しないと、配信は失敗としてマークされ、リトライされます。
ジッター付き指数バックオフ
プロバイダーは通常、指数バックオフでリトライします:最初のリトライは1分後、次に5分後、30分後などです。ジッター(ランダムな変動)を追加すると、多くのWebhookが同時に失敗した場合のサンダリングハードを防ぎます。
Webhook送信側を構築する場合は、次のようにバックオフを実装します:
- リトライ1:10秒後
- リトライ2:1分後
- リトライ3:5分後
- リトライ4:30分後
- リトライ5:2時間後
最大試行回数に達したら、イベントを手動検査用のデッドレターキューに移動します。
冪等性:重複配信の処理
リトライにより、エンドポイントが同じイベントを複数回受信する可能性があります。処理は冪等でなければなりません:同じイベントを2回処理しても、1回処理した場合と同じ効果である必要があります。
一般的な戦略:
- イベントIDの重複排除: 処理済みイベントIDをデータベースやキャッシュに保存します。処理前に、IDが既に見たものであるかを確認します。
- 挿入ではなくアップサート: 重複を自然に処理するデータベース操作を使用します(例:
INSERT ... ON CONFLICT DO NOTHING)。 - 冪等キー: カードへの課金などの操作では、一意のキーを渡して決済プロバイダーが重複リクエストを無視するようにします。
Webhookコンシューマーのベストプラクティス
| プラクティス | 重要な理由 |
|---|---|
| 署名を検証する | 偽造リクエストを防ぐ |
| 迅速に応答する(2秒以内) | タイムアウトと不要なリトライを避ける |
| 非同期で処理する | 受信と重い処理を分離する |
| 冪等ハンドラーを使用する | 重複配信に対して安全 |
| すべてのイベントをログに記録する | デバッグと監査に役立つ |
| 失敗を監視する | 問題が拡大する前に検出する |
また、リプレイ攻撃を防ぐために署名のタイムスタンプを検証することも検討してください。多くのプロバイダーは署名付きペイロードにタイムスタンプを含めています。数分以上古いイベントは拒否します。
Webhookプロデューサーのベストプラクティス
Webhookを送信するサービスを構築する場合は、次のガイドラインに従ってください:
- すべてのペイロードをHMAC-SHA256で署名する。
- 一意のイベントIDとタイムスタンプを含める。
- ジッター付き指数バックオフでリトライする。
- コンシューマーが配信ログを表示し、イベントを手動でリプレイできる方法を提供する。
- 署名検証プロセスを明確に文書化する。
よくある質問
署名付きWebhookペイロードとは何ですか?
署名付きペイロードには、リクエストボディと共有シークレットから計算された暗号署名(通常はHMAC)が含まれます。受信者は署名を検証して、リクエストが真正で改ざんされていないことを確認します。
Webhookは何回リトライされるべきですか?
普遍的な数はありませんが、ほとんどのプロバイダーは数時間にわたって3〜5回リトライします。その後、イベントは通常失敗としてマークされ、手動リプレイが可能になる場合があります。リトライ回数に関係なく、システムは重複を冪等に処理する必要があります。
HTTPSなしでWebhookを使用できますか?
技術的には可能ですが、常にHTTPSを使用すべきです。Webhookには機密データが含まれることが多く、TLSがなければ攻撃者がペイロードを傍受または改ざんする可能性があります。HTTPSは、意図したサーバーに送信していることも保証します。
NginxログでWebhookをデバッグする
Webhookが失敗した場合、最初に見るべきはWebサーバーログです。Nginxを使用している場合、アクセスログとエラーログにすべてのリクエストが記録され、ステータスコードと応答時間が含まれます。繰り返しの401(署名検証の失敗)、500(アプリケーションエラー)、タイムアウトなどのパターンをすぐに発見できます。
より迅速な分析には、Nginx Log Analyzerを使用してWebhookトラフィックを解析・要約し、ステータスコードでフィルタリングし、問題のあるエンドポイントを特定してください。