Webhook解説:署名付きペイロードとリトライ

Backend2026-11-06TryQuickToolBox

決済ゲートウェイやCI/CDサービスを統合し、リアルタイムのイベントを受信する必要があるとします。Webhookは標準的なソリューションですが、2つの大きな課題があります。リクエストが本当に期待される送信元から来たかを検証すること、そしてエンドポイントがダウンしたり遅くなったりしたときの障害を処理することです。この記事では、署名付きWebhookペイロードと堅牢なリトライロジックを実装する方法を、実用的なコード例とともに説明します。

Webhookとは?

Webhookはユーザー定義のHTTPコールバックです。アプリケーションがAPIをポーリングして更新を確認する代わりに、イベントが発生するたびにプロバイダーが指定したURLにHTTP POSTリクエストを送信します。これにより、統合が効率的でほぼリアルタイムになります。

しかし、Webhookはプッシュベースであるため、エンドポイントは公開されている必要があり、予期しない負荷に対処し、重複したリクエストや悪意のあるリクエストを処理する必要があります。ここで署名とリトライが重要になります。

Webhookペイロードに署名する理由

検証がなければ、Webhook URLを知っている誰でも偽のイベントを送信できます。共有シークレットでペイロードに署名することで、送信者を認証し、ペイロードが改ざんされていないことを確認できます。

最も一般的なアプローチは、SHA-256を使用したHMAC(Hash-based Message Authentication Code)です。プロバイダーは秘密鍵を使用して生のリクエストボディに対して署名を計算し、ヘッダー(例:X-Signature)に含めます。サーバーは署名を再計算して比較します。

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; // 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);
  console.log('Received event:', event.type);
  res.status(200).send('OK');
});

app.listen(3000, () => console.log('Webhook server running'));

重要なポイント:

リトライの処理:Webhookを信頼性の高いものにする

ネットワークは失敗し、サーバーはクラッシュし、デプロイによってダウンタイムが発生します。信頼性の高いWebhookシステムは、失敗した配信をリトライする必要があります。しかし、単純なリトライは重複処理を引き起こし、システムに過負荷をかける可能性があります。

リトライ戦略

一般的なリトライパターンには次のものがあります。

ほとんどのプロバイダーは、24時間にわたって指数バックオフで最大5回リトライするなどを実装しています。

冪等性:安全なリトライの鍵

リトライは同じイベントを複数回配信する可能性があるため、エンドポイントは冪等である必要があります。ペイロードに一意のイベントIDを含め、処理済みのIDを保存して重複をスキップします。

簡単なインメモリセットを使用した例(本番環境ではデータベースを使用してください):

const processedEvents = new Set();

app.post('/webhook', (req, res) => {
  // ... 署名検証 ...
  const event = JSON.parse(payload);
  if (processedEvents.has(event.id)) {
    return res.status(200).send('Already processed');
  }
  // イベントを処理
  processedEvents.add(event.id);
  res.status(200).send('OK');
});

本番環境では、イベントIDに一意制約を設定したデータベースを使用してください。

Webhookコンシューマーのベストプラクティス

リトライポリシーの比較

ポリシー 長所 短所
固定間隔 実装が簡単 復旧中のシステムに過負荷をかける可能性
指数バックオフ 障害時の負荷を軽減 一時的な失敗の場合、配信が遅れる
ジッター付き指数バックオフ 同期したリトライを防止 やや複雑

FAQ

Webhook署名を検証するにはどうすればよいですか?

共有シークレットを使用して生のリクエストボディのHMACを計算し、タイミングセーフな関数を使用して署名ヘッダーと比較します。単純な等価比較は決して使用しないでください。

Webhookエンドポイントがダウンしている場合はどうすればよいですか?

プロバイダーは指数バックオフでリトライする必要があります。ダウンタイムの影響を最小限に抑えるために、エンドポイントが迅速に2xxを返し、イベントを非同期で処理するようにしてください。

署名なしでWebhookを使用できますか?

推奨されません。署名がないと送信者を検証できず、エンドポイントが偽のイベントに対して脆弱になります。利用可能な場合は常に署名を使用してください。

Webhookペイロードをテストする際には、JSONデータを検査する必要があるかもしれません。JSON Formatterを使用して、ペイロードをすばやく整形して検証できます。