Webhook解説:署名付きペイロードとリトライ
APIを構築し、イベントが発生したときに他のサービスに通知する必要が出てきました。Webhookエンドポイントを設定しましたが、すぐに誰でも偽のペイロードを送信できること、そしてサーバーがダウンしているとイベントが永久に失われることに気づきました。これはよくある落とし穴です。Webhookは概念としてはシンプルですが、セキュリティと信頼性のために慎重な設計が必要です。
このガイドでは、Webhookの基本を説明します。ペイロードに署名して改ざんを防ぐ方法、受信側で署名を検証する方法、指数バックオフを使ったリトライロジックを実装して障害を処理する方法です。また、重複処理を避けるための冪等性についても取り上げます。読み終える頃には、信頼できる堅牢なWebhookシステムを構築できるでしょう。
Webhookとは?
Webhookはユーザー定義のHTTPコールバックです。ソースシステムでイベントが発生すると(例:支払い処理)、指定したURLにHTTP POSTリクエストを送信し、イベントの詳細を含むペイロードを渡します。これにより、ポーリングなしでサービス間のリアルタイム通信が可能になります。
しかし、Webhookには2つの課題があります:
- セキュリティ:受信側はリクエストが本当に送信者から来たものかをどうやって知るのか?
- 信頼性:受信側が一時的に利用できない場合はどうするか?イベントが最終的に配信されることをどう保証するか?
署名付きペイロードでWebhookを保護する
偽造リクエストを防ぐために、送信者は共有シークレットを使ってペイロードに署名する必要があります。受信側は署名を検証して送信者を認証できます。最も一般的な方法は、SHA-256を使用したHMAC(Hash-based Message Authentication Code)です。
HMAC署名の仕組み
- 送信者と受信者は秘密鍵(理想的には長いランダム文字列)を共有します。
- 各Webhookについて、送信者は秘密鍵を使ってリクエストボディのHMACを計算し、ヘッダー(例:
X-Signature)に含めます。 - 受信側は受信したボディに対して同じHMACを計算し、ヘッダーの値と比較します。一致すれば、リクエストは本物です。
JSONシリアライズの違いによる不一致を避けるため、パースされたバージョンではなく生のリクエストボディに署名することが重要です。
例:Node.jsでペイロードに署名する
const crypto = require('crypto');
function signPayload(payload, secret) {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
// 送信側:
const payload = JSON.stringify({ event: 'payment.success', amount: 100 });
const signature = signPayload(payload, 'your-secret-key');
// ヘッダーに署名を含める:X-Signature: signature
受信側で署名を検証する
受信側では、ペイロードを処理する前に署名を検証する必要があります。タイミング攻撃を防ぐために定数時間比較を使用してください。
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));
}
// Webhookエンドポイント内:
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.rawBody; // 生のボディ(文字列)
if (!verifySignature(payload, signature, 'your-secret-key')) {
return res.status(401).send('Invalid signature');
}
// イベントを処理
res.status(200).send('OK');
});
注意:中間者攻撃を防ぐために常にHTTPSを使用してください。また、リプレイ攻撃を防ぐために署名付きペイロードにタイムスタンプを含めることを検討してください。
指数バックオフによるリトライの実装
信頼性の高い受信側であっても、ネットワークの問題や一時的な障害によりWebhookの配信が失敗することがあります。堅牢な送信者は失敗した配信をリトライする必要があります。しかし、単純なリトライは受信側を圧倒したり、重複処理を引き起こしたりする可能性があります。解決策は、ジッター付きの指数バックオフと最大リトライ回数です。
リトライ戦略
- 失敗時のリトライ:受信側が非2xxステータスコードを返すかタイムアウトした場合にリトライします。
- 指数バックオフ:リトライ間の遅延を指数関数的に増やします(例:1秒、2秒、4秒、8秒...)。
- ジッター:サンダリングハード問題を避けるために遅延にランダムな変動を加えます。
- 最大試行回数:制限(例:5回のリトライ)を設定し、その後Webhookを失敗としてマークします(管理者にアラートを送るなど)。
例:Node.jsでのリトライロジック
async function deliverWebhook(url, payload, secret, maxRetries = 5) {
const signature = signPayload(payload, secret);
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Signature': signature },
body: payload,
});
if (response.ok) return;
throw new Error(`HTTP ${response.status}`);
} catch (error) {
attempt++;
if (attempt >= maxRetries) throw error;
const delay = Math.min(1000 * 2 ** attempt + Math.random() * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
この関数は指数バックオフとジッターで最大5回リトライし、遅延を30秒に制限します。必要に応じてパラメータを調整してください。
冪等性の確保
リトライは重複配信を引き起こす可能性があるため、Webhookハンドラーは冪等でなければなりません。ペイロードに一意のイベントIDを含め、受信側で処理済みIDを追跡して二重処理を避けます。
// 受信側:
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const { id } = req.body;
if (processedEvents.has(id)) {
return res.status(200).send('Already processed');
}
// イベントを処理
processedEvents.add(id);
res.status(200).send('OK');
});
本番環境では、再起動をまたいで処理済みIDを追跡するために永続ストア(Redisやデータベースなど)を使用してください。
比較:署名付き vs. 署名なしWebhook
| 側面 | 署名なし | 署名付き(HMAC) |
|---|---|---|
| セキュリティ | 誰でもリクエストを偽造可能 | 秘密鍵の保持者のみ有効な署名を作成可能 |
| 整合性 | ペイロードが改ざんされる可能性 | 改ざんは署名を無効にする |
| 複雑さ | 低 | 中程度(鍵管理、検証) |
| ユースケース | 内部、信頼されたネットワーク | あらゆる公開Webhook |
FAQ
Webhook署名とは何ですか?
Webhook署名は、共有シークレットで作成されたペイロードの暗号化ハッシュ(通常はHMAC)です。これにより、受信側はリクエストが期待される送信者から来たものであり、ペイロードが変更されていないことを検証できます。
失敗したWebhookを何回リトライすべきですか?
固定の数はありませんが、一般的な慣行は指数バックオフで3〜5回のリトライです。その後、配信を失敗としてマークし、チームに警告します。イベントの重要度を考慮してください。支払い通知は重要でない更新よりも多くのリトライが必要かもしれません。
HTTPSなしでWebhookを使用できますか?
本番環境では平文HTTP経由でWebhookを使用すべきではありません。HTTPSは転送中のペイロードを暗号化し、盗聴や中間者攻撃を防ぎます。署名があっても、HTTPは機密データを露出します。
Webhookペイロードをテストする準備はできましたか?送信やデバッグの前に、JSON Formatterを使ってJSONペイロードを整形して検証しましょう。