Webhook解説:署名付きペイロードとリトライ
APIを構築し、イベントが発生したときに他のサービスへ通知する必要が出てきました。Webhookエンドポイントを設定したものの、HTTPリクエストを確実に配信するのは思ったより難しいとすぐに気づくでしょう。ネットワーク障害、悪意ある攻撃者、重複イベントが混乱を引き起こす可能性があります。このガイドでは、署名付きペイロードとリトライを備えたWebhookを実装し、堅牢な連携を構築する方法を解説します。
Webhookとは何か?
Webhookはユーザー定義のHTTPコールバックです。アプリケーションでイベントが発生すると、購読者が提供したURLへHTTP POSTリクエストを送信します。これにより、ポーリングなしでサービス間のリアルタイム通信が可能になります。
例えば、決済処理サービスは課金が成功したときにアプリへWebhookを送信できます。アプリはその場で注文ステータスを更新できます。
なぜWebhookペイロードに署名するのか?
Webhookエンドポイントは公開されているため、誰でも偽のリクエストを送信できます。検証を行わないと、アプリが不正なイベントを処理してしまう可能性があります。ペイロードに署名することで、リクエストが自サービスから発信され、改ざんされていないことを保証できます。
署名には通常、送信者と受信者の間で共有されるシークレットを使用します。送信者はペイロードに対してハッシュベースのメッセージ認証コード(HMAC)を計算し、ヘッダーに含めます。受信者はHMACを再計算して比較します。
署名の生成と検証方法
ほとんどの実装ではHMAC-SHA256を使用します。以下はPythonでの簡単な例です:
import hmac
import hashlib
import json
def generate_signature(payload, secret):
body = json.dumps(payload, separators=(',', ':')).encode('utf-8')
signature = hmac.new(secret.encode('utf-8'), body, hashlib.sha256).hexdigest()
return signature
def verify_signature(payload, signature, secret):
expected = generate_signature(payload, secret)
return hmac.compare_digest(expected, signature)
Webhookを送信するときは、X-Signatureのようなヘッダーに署名を含めます。受信者は生のボディを抽出し、HMACを再計算して比較します。タイミング攻撃を防ぐため、常に定数時間比較関数を使用してください。
確実な配信のためのリトライ実装
Webhookはインターネット経由で送信されるため、失敗は避けられません。サーバーがダウンしている、ネットワークが遅い、購読者がエラーを返すといったことが起こります。指数バックオフを伴うリトライが不可欠です。
リトライ戦略
一般的なアプローチは、遅延を増やしながらリトライすることです:1分、5分、30分、2時間など。最大試行回数(例:10回)または合計時間枠(例:24時間)に達したら停止します。
特定の条件でのみリトライします:ネットワークエラー、5xxレスポンス、429(Too Many Requests)。4xxエラー(429を除く)ではリトライしないでください。これらはクライアント側のミスを示しており、リトライしても解決しないためです。
リトライロジックの例
以下はrequestsライブラリを使用したPythonでの簡略化されたリトライ機構です:
import requests
import time
from requests.exceptions import RequestException
def send_webhook(url, payload, secret, max_retries=5):
headers = {'Content-Type': 'application/json'}
signature = generate_signature(payload, secret)
headers['X-Signature'] = signature
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, headers=headers, timeout=10)
if response.status_code < 500 and response.status_code != 429:
return response.status_code
except RequestException:
pass
# Exponential backoff with jitter
delay = (2 ** attempt) + (random.random() * 0.1)
time.sleep(delay)
return None # Failed after retries
本番環境では、失敗した試行をキューに保存し、非同期で処理します。Celeryやクラウドキュー(AWS SQS、Google Cloud Tasks)などのツールが役立ちます。
Webhookセキュリティのベストプラクティス
- HTTPSを使用する:盗聴を防ぐため、常にTLS経由でWebhookを送信します。
- タイムスタンプを含める:ペイロードにタイムスタンプを追加し、数分以上前のリクエストを拒否してリプレイ攻撃を防ぎます。
- シークレットをローテーションする:署名シークレットを定期的に変更し、ローテーション中は複数の有効なシークレットをサポートします。
- URLを検証する:Webhookを登録するとき、URLに到達可能でテストリクエストに2xxを返すことを確認します。
- レート制限:購読者ごとのリクエスト数を制限して、Webhook送信者が悪用されるのを防ぎます。
重複と冪等性の処理
リトライは重複配信を引き起こす可能性があります。ペイロードに一意のイベントIDを含め、受信者側で処理済みIDを追跡します。イベントIDが以前に見られた場合は無視します。
あるいは、イベントハンドラーを冪等に設計します。例えば、同じデータでレコードを複数回更新しても、1回行うのと同じ結果になるようにします。
Webhookのテスト
ngrokなどのツールを使ってローカルサーバーを公開し、Webhook配信をテストします。エラーコードを返して失敗をシミュレートし、リトライが期待どおりに行われることを確認します。
FAQ
署名付きWebhookペイロードとは何ですか?
署名付きペイロードには暗号署名(通常はHMAC)が含まれ、受信者はリクエストが信頼できる送信元から来て改ざんされていないことを検証できます。
失敗したWebhookは何回リトライすべきですか?
信頼性の要件によります。一般的な慣行は、24時間にわたって指数バックオフで5〜10回リトライすることです。購読者が手動でリトライをトリガーできるようにもしてください。
署名なしでWebhookを使用できますか?
できますが、リスクがあります。署名がないと、誰でも偽のイベントを送信できます。不正を防ぐため、本番環境では常にWebhookに署名してください。
Webhookペイロードをテストする準備はできましたか?送信前にJSONペイロードを検証して整形するには、JSON Formatterをご利用ください。