サイトのAPI図鑑B版
掲載情報が正確でない可能性があります。
開発者向けAPIツール

Webhookの実装ガイド【受信・署名検証・リトライ・べき等性の設計】

Webhookの仕組みとセキュアな受信処理(HMAC署名検証)・リトライ設計・べき等性の実装・デバッグ方法を解説します。GitHub・Stripe・Shopifyのwebhook実装例も紹介します。

#Webhook#HMAC#署名検証#イベント駆動

Webhookとは

Webhookは「イベント発生時にHTTP POSTで通知を送る」仕組みです。決済完了・GitHub push・新規注文などのイベントが発生したとき、サービスが設定されたURLにJSONペイロードをPOSTします。受信側のアプリケーションはこの通知を受けてアクション(メール送信・DB更新等)を実行します。

Webhook受信エンドポイントの実装

import crypto from 'crypto';
import express from 'express';

const app = express();

// Webhookエンドポイントではrawボディが必要
app.post('/webhooks/stripe', 
  express.raw({ type: 'application/json' }), 
  async (req, res) => {
    const signature = req.headers['stripe-signature'];
    
    // 1. HMAC署名の検証
    let event;
    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        signature,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (err) {
      console.error('署名検証失敗:', err.message);
      return res.status(400).json({ error: '不正なWebhook' });
    }
    
    // 2. べき等性の確認(同じイベントIDを2回処理しない)
    const alreadyProcessed = await db.processedEvents.findUnique({
      where: { eventId: event.id }
    });
    if (alreadyProcessed) {
      return res.status(200).json({ status: 'already_processed' });
    }
    
    // 3. イベントタイプごとの処理
    try {
      switch (event.type) {
        case 'payment_intent.succeeded':
          await handlePaymentSucceeded(event.data.object);
          break;
        case 'customer.subscription.deleted':
          await handleSubscriptionCancelled(event.data.object);
          break;
        default:
          console.log(`未処理のイベント: ${event.type}`);
      }
      
      // 4. 処理済みとして記録
      await db.processedEvents.create({
        data: { eventId: event.id, processedAt: new Date() }
      });
      
      res.status(200).json({ received: true });
    } catch (err) {
      console.error('Webhook処理エラー:', err);
      // 500を返すとStripeがリトライする
      res.status(500).json({ error: 'Processing failed' });
    }
  }
);

独自Webhookのセキュア実装

// Webhookを送信する側(署名の生成)
const generateHmacSignature = (payload, secret) => {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  return `sha256=${hmac.digest('hex')}`;
};

const sendWebhook = async (url, event, payload) => {
  const signature = generateHmacSignature(payload, process.env.WEBHOOK_SECRET);
  
  await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature,
      'X-Webhook-Event': event,
      'X-Webhook-Delivery': crypto.randomUUID() // イベントID
    },
    body: JSON.stringify(payload)
  });
};

// 受信側(署名の検証)
const verifyWebhookSignature = (body, signature, secret) => {
  const expected = generateHmacSignature(JSON.parse(body), secret);
  // タイミング攻撃を防ぐためcrypto.timingSafeEqualを使用
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
};

Webhookのリトライ戦略

Webhookの送信側は受信側が失敗(non-200レスポンス・タイムアウト)した場合に指数バックオフでリトライします。

  • 1回目: 即時
  • 2回目: 1分後
  • 3回目: 5分後
  • 4回目: 30分後
  • 5回目: 2時間後

ローカル開発でのWebhookデバッグ

# ngrokを使ってローカルのWebhookエンドポイントを外部に公開
ngrok http 3000

# Stripe CLIを使ってStripeのWebhookをローカルに転送
stripe listen --forward-to localhost:3000/webhooks/stripe

まとめ

Webhookの実装で最も重要なのはHMAC署名検証とべき等性です。署名検証を省略すると偽のリクエストによる不正な処理が発生するリスクがあります。べき等性を実装してリトライによる二重処理を防ぎ、非同期でイベント処理をキューに入れることで高いスループットと信頼性を実現してください。

よくある質問

Q.WebhookとPollingの違いは何ですか?

Pollingはクライアントが定期的にサーバーにデータ変更を問い合わせる方式です。Webhookはサーバー側でイベントが発生したときにクライアントのURLにHTTP POSTで通知する方式です。WebhookはPollingより低レイテンシ・低負荷でリアルタイムな更新を受け取れます。

Q.WebhookのHMAC署名検証はなぜ必要ですか?

Webhookエンドポイントはインターネットに公開されているため、悪意のある第三者からの偽のリクエストを防ぐ必要があります。送信元の秘密鍵でHMACを生成してリクエストヘッダーに付与し、受信側で同じHMACを計算して一致するかを検証することで、正規の送信元からのリクエストであることを確認できます。

Q.Webhookのべき等性とはどういう意味ですか?

べき等性とは、同じリクエストを複数回処理しても結果が変わらないことです。Webhookは配信保証のためにリトライされる場合があり、同じイベントが複数回届く可能性があります。イベントIDで処理済みのイベントを記録し、重複処理を防ぐ実装が必要です。

関連記事