Stripe Webhook を Next.js で安全に実装する#
Stripe の Webhook エンドポイントを実装する際、署名検証を正しく行わないと、第三者が偽の決済完了通知を送り込める。Next.js 環境での実装で遭遇した落とし穴と対処。
Webhook の基本フロー#
sequenceDiagram
participant U as ユーザー
participant S as Stripe
participant W as Webhook Endpoint
participant DB as DB
U->>S: 決済
S->>S: 処理完了
S->>W: POST /api/webhook<br/>+ Stripe-Signature header
W->>W: 署名検証
alt 検証 OK
W->>DB: 注文状態更新
W->>S: 200 OK
else 検証 NG
W->>S: 400 Bad Request
end
遭遇した問題#
1. Next.js App Router で raw body が取れない
Stripe の署名検証には 生のリクエストボディ(parsing 前のバイト列)が必要。App Router の request.json() は内部でパースしてしまい、生バイトが失われる。
-
対策:
request.text()で文字列として取り、その文字列をそのままstripe.webhooks.constructEvent()に渡す
2. 冪等性の担保
Stripe は同じイベントを複数回送ってくることがある(ネットワーク失敗時のリトライ等)。同じ event.id で同じ処理を 2 回走らせると二重課金になる。
- 対策:
event.idを DB に保存し、既に処理済みならスキップする。ProcessedEventsテーブルで一意制約を張る
3. ローカル開発でのテスト
開発環境で Webhook を受けるには、Stripe CLI でトンネルを張る。
stripe listen --forward-to localhost:3000/api/webhook
4. シークレットの漏洩
STRIPE_WEBHOOK_SECRET は環境変数管理。.env.local は必ず gitignore。クライアントコード(NEXT_PUBLIC_*)には入れない。
処理の冪等化パターン#
flowchart TD
E[Webhook 受信] --> V{署名検証}
V -->|NG| R1[400 を返して終了]
V -->|OK| D{event.id が<br/>処理済み?}
D -->|はい| R2[200 を返して終了]
D -->|いいえ| P[処理実行]
P --> M[event.id を記録]
M --> R3[200 を返して終了]
チェックリスト#
- [ ] 署名検証を実装した
- [ ] raw body を使っている(パース前)
- [ ]
event.idで冪等化している - [ ]
STRIPE_WEBHOOK_SECRETを環境変数で管理 - [ ] 検証失敗時に 4xx を返す(2xx を返さない)
- [ ] 処理が遅い場合はキューに逃がし、Webhook は即 200 を返す
学び#
- 署名検証の実装を省略しない。テスト用に検証を一時的に外すと、そのまま本番に入るリスクがある
- Webhook は at-least-once 配信と割り切り、冪等化を最初から組み込む
- Webhook エンドポイントの処理時間は短く。重い処理はキューに逃がして非同期化する