そのままだと Django はすべての Webhook POST を 403 で拒否します。犯人は CSRF ミドルウェアが仕事をしていること——クロスサイトリクエストフォージェリから守ること——ですが、プロバイダーはブラウザではなく、セッション Cookie も CSRF トークンも持ちません。対処はデコレータ 1 つと、リクエストボディへのアクセスに関する 1 行の意識です。
Webhook ビューで CSRF をスキップ
Django 流のやり方:
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse, HttpResponseBadRequest
import hmac, hashlib
@csrf_exempt
def stripe_webhook(request):
if request.method != 'POST':
return HttpResponseBadRequest('POST only')
payload = request.body # 生のバイト列、未加工
signature = request.headers.get('Stripe-Signature', '')
# 署名検証(実コードでは stripe SDK を使う)
if not verify(payload, signature, settings.STRIPE_WEBHOOK_SECRET):
return HttpResponse(status=401)
event = json.loads(payload)
handle_stripe_event(event)
return HttpResponse(status=200)
@csrf_exempt デコレータはこのビューだけミドルウェアのチェックを外します——他のフォームは保護されたままです。request.body は届いたままの生バイト列を返し、これこそ署名検証が必要とするものです。
Django REST Framework を使う場合
DRF はリクエストを独自の Request オブジェクトでラップし、content-type に応じてパーサーを走らせます。ビューのコードが request.data を読む頃には、JSON パーサーがすでにボディを消費しています。出口は 2 つ:
選択肢 A: 素の Django ビューのままにする
Webhook ハンドラーに DRF のパーサー、ブラウザブル API、認証機構は不要です。DRF エンドポイントの隣に素の @csrf_exempt ビューを置くのが最もきれいなパターンです。
選択肢 B: 生アクセスで DRF を使う
どうしても DRF を使うなら、request.data ではなく request.body(背後の Django リクエスト)を読みます:
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import AllowAny
@api_view(['POST'])
@authentication_classes([]) # Webhook には認証なし
@permission_classes([AllowAny]) # プロバイダーは署名で認証済み
def stripe_webhook(request):
payload = request._request.body # 背後の Django リクエスト、生バイト列
signature = request.META.get('HTTP_STRIPE_SIGNATURE', '')
# ...検証して処理
request._request という間接参照はぎこちないですが、DRF のパースを回避する必要があるときの公式の逃げ道です。
トンネルをセットアップ
- Django 開発サーバーを起動:
python manage.py runserver 8000。 - トンネルのホスト名を
settings.pyのALLOWED_HOSTSに追加します。ローカル開発では['*']が最も簡単、好みなら特定のトンネルドメインでも構いません。 - 別ターミナルで
npx portpreview 8000。 - トンネル URL に Webhook のパスを付けてプロバイダーのダッシュボードに貼り付けます。
- テストイベントを発火し、ビューに届くのを確認します。
ALLOWED_HOSTS を忘れると、原因の見えない汎用的な Django 400 が出ます。最初の Webhook 配信が 400 を返し、ビューのログが見えないなら、それが理由です。
よくある落とし穴
ボディを 2 回読む
どのフレームワークもそうですが、Django はボディを一度しか読めないストリームとして渡します。ミドルウェアやロギングのラッパーがすでに request.body に触れていると、ビューのコードは空のバイト列を見ます。確認のために len(request.body) を出力しましょう。
タイムゾーンとタイムスタンプの許容
署名されたタイムスタンプを datetime.now() と比較するなら、timezone.now() を使うか UTC で比較してください。naive な datetime の比較は夏時間の切り替え時に断続的に失敗します。
async ビュー
Django 4+ は async def ビューに対応します。Webhook ハンドラーは async にできますが、署名検証 SDK(Stripe Python など)は同期であることに注意してください。asgiref.sync_to_async で実行するか、ビュー全体を同期に保ちましょう。
本番デプロイの注意
ローカルテストを過ぎれば、同じビューが本番でも動きます。Gunicorn、uWSGI、Daphne——どれもビューが見る前に request.body に触れません。リバースプロキシは触れるかもしれませんが(例えば proxy_buffering を有効にした nginx)、それはバッファリングの話で、ボディの改変ではありません。
署名の基礎理論は 署名検証ガイドを参照。Stripe 固有のパターンはダッシュボード側を Stripe ローカルテストガイドがカバーします。Python 開発サーバーと相性のよいトンネルは PortPreview のウェイトリストへ。