Webhook解説:署名付きペイロードとリトライ
サードパーティサービスからイベントを受け取るWebhookエンドポイントを構築したとします。テストではすべてうまくいきますが、本番環境では重複イベント、通知の欠落、偽造リクエストに関するセキュリティ上の懸念に気づくでしょう。これらは適切な保護策なしにWebhookを実装する際のよくある落とし穴です。
Webhookは、何かが起こったときにサービスがアプリケーションに通知するためのHTTPコールバックです。ポーリングとは異なり、リアルタイムの更新を提供します。しかし、信頼性高く使用するには、2つの重要な側面に対処する必要があります:真正性(リクエストが期待される送信者から来たことを確認する)と配信(失敗とリトライを処理する)です。この記事では、実用的なコード例を交えて両方を説明します。
Webhookの仕組み
Webhookはユーザー定義のHTTP POSTエンドポイントです。イベントが発生すると、ソースサービスはそのURLにイベントを記述したペイロードを含むリクエストを送信します。たとえば、決済処理サービスは課金が成功したときにWebhookを送信するかもしれません。
流れはシンプルです:
- サービスにWebhook URLを登録します。
- イベントが発生すると、サービスはそのURLにHTTP POSTを送信します。
- エンドポイントはペイロードを処理し、受信を確認する2xxステータスを返します。
適切な検証がなければ、誰でもエンドポイントにリクエストを送信できます。リトライ処理がなければ、一時的な障害でイベントが失われる可能性があります。それぞれに対処しましょう。
署名付きペイロードでWebhookを保護する
Webhookが期待される送信者から発信されたことを保証するため、サービスは共有シークレットを使用してペイロードに署名します。署名は通常、リクエストボディのHMAC(Hash-based Message Authentication Code)であり、X-Signatureのようなヘッダーで送信されます。
HMAC署名の仕組み
送信者は秘密鍵と生のリクエストボディを使用してHMACを計算し、16進数エンコードされた署名をヘッダーに含めます。エンドポイントは同じ秘密鍵でHMACを再計算し、受信した署名と比較します。一致すれば、リクエストは真正です。
Expressを使用したNode.jsの最小例を次に示します:
const crypto = require('crypto');
const express = require('express');
const app = express();
// 署名検証には生のボディを使用
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body;
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);
// ... イベントを処理
res.status(200).send('OK');
});
重要なポイント:
- 検証には必ず生のリクエストボディを使用し、パースされたオブジェクトは使わないでください。パースするとペイロードが変更され(例:空白)、署名が壊れる可能性があります。
- タイミング攻撃を防ぐため、タイミングセーフな比較を使用してください。
- シークレットは安全に保管し(環境変数、シークレットマネージャー)、定期的にローテーションしてください。
追加のセキュリティ対策
- タイムスタンプ検証:署名にタイムスタンプを含め(例:
t=timestamp,v1=signature)、数分以上前のリクエストを拒否してリプレイ攻撃を防ぎます。 - IP許可リスト:プロバイダーがIP範囲を公開している場合、エンドポイントをそれらのIPに制限します。
- HTTPS:転送中のペイロードを保護するため、常にHTTPSを使用してください。
信頼性の高いリトライの実装
Webhookの配信は保証されません。ネットワーク障害、サーバーダウン、または非2xxステータスの返却が起こり得ます。これに対処するため、送信者は通常、指数バックオフでリトライします。受信者として、エンドポイントを冪等に設計し、迅速に応答する必要があります。
送信側の場合(Webhookを送信する場合)
Webhookを送信するサービスを構築している場合、バックオフ付きのリトライを実装してください。一般的な戦略:
- ネットワークエラーまたは5xxレスポンスでリトライ。
- 4xxレスポンス(429 Too Many Requestsを除く)ではリトライしない。
- ジッター付き指数バックオフを使用:1秒、2秒、4秒、8秒…と最大まで待機。
- 最大リトライ回数を設定(例:5回)し、失敗用のデッドレターキューを検討。
Pythonでのリトライロジックの例:
import time
import requests
from requests.exceptions import RequestException
def send_webhook(url, payload, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, timeout=5)
if 200 <= response.status_code < 300:
return True
elif 400 <= response.status_code < 500 and response.status_code != 429:
# クライアントエラー、リトライしない
return False
except RequestException:
pass
# ジッター付き指数バックオフ
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
受信側の場合
エンドポイントは次のようにする必要があります:
- 迅速に確認応答:数秒以内に2xxを返します。処理に時間がかかる場合は、イベントをキューに入れて非同期で処理します。
- 冪等性を保つ:ペイロードのイベントIDを使用して重複排除します。処理済みIDを保存し、繰り返しを無視します。
- 同時実行を処理:同じイベントを同時に処理する際の競合状態を避けるため、データベーストランザクションまたはロックを使用します。
データベースの一意制約を使用した冪等性の例:
CREATE TABLE processed_events (
event_id VARCHAR(255) PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
処理前にイベントIDの挿入を試みます。主キー違反が発生した場合は処理をスキップします。
ローカルでのWebhookテスト
開発中、サービスがローカルサーバーに到達するための公開URLが必要です。ngrokやlocaltunnelなどのツールがlocalhostを公開します。開発中は常にテスト用シークレットを使用し、署名を検証して問題を早期に発見してください。
比較:ポーリング vs Webhook
| 側面 | ポーリング | Webhook |
|---|---|---|
| レイテンシ | 間隔に依存 | ほぼリアルタイム |
| サーバー負荷 | 高い(頻繁なリクエスト) | 低い(イベント時のみ) |
| 複雑さ | 実装が簡単 | 署名検証とリトライ処理が必要 |
| 信頼性 | リトライを自分で制御 | 送信者のリトライポリシーに依存 |
FAQ
署名付きWebhookペイロードとは何ですか?
署名付きペイロードには、共有シークレットで生成された暗号署名(通常はHMAC)が含まれます。これにより、受信者はリクエストが期待される送信者から来たものであり、改ざんされていないことを検証できます。
重複したWebhook配信を処理するにはどうすればよいですか?
ペイロードの一意のイベントIDを使用してエンドポイントを冪等にします。処理済みIDを保存し、重複を無視します。これにより、リトライやネットワーク問題が二重処理を引き起こさないようにします。
失敗したWebhook配信をリトライすべきですか?
はい、ただし一時的なエラー(ネットワーク問題、5xx)のみです。ジッター付き指数バックオフを使用し、最大リトライ回数を設定します。4xxエラー(429を除く)はリクエスト自体に問題があることを示すため、リトライを避けてください。
Webhookペイロードをデバッグする際、JSONデータを検査する必要がよくあります。JSON Formatterは、JSONペイロードを迅速に整形して検証するのに役立ちます。