Wszystkie artykuły
DjangoPythonwebhook debugginglocal testing

Webhooki Django: przejść CSRF i trafić do handlera

Domyślnie Django odrzuca każdy POST webhooka kodem 403. Winowajcą jest middleware CSRF robiący swoje — chroni przed cross-site request forgery — tyle że dostawca nie jest przeglądarką i nie niesie ciasteczka sesji ani tokenu CSRF. Rozwiązanie to jeden dekorator i jedna linijka świadomości o dostępie do ciała żądania.

Pomiń CSRF w widokach webhooków

Po django'owemu:

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            # surowe bajty, nietknięte
    signature = request.headers.get('Stripe-Signature', '')

    # Weryfikuj podpis (w prawdziwym kodzie użyj SDK stripe)
    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)

Dekorator @csrf_exempt usuwa sprawdzanie middleware tylko dla tego widoku — pozostałe formularze nadal są chronione. request.body zwraca surowe bajty tak, jak przyszły, czyli dokładnie to, czego potrzebuje weryfikacja podpisu.

Jeśli używasz Django REST Framework

DRF opakowuje żądania we własny obiekt Request i uruchamia parsery zależnie od content-type. Zanim twój kod widoku odczyta request.data, parser JSON już skonsumował ciało. Dwa wyjścia:

Opcja A: zostaw jako zwykły widok Django

Handlery webhooków nie potrzebują parserów DRF, przeglądalnego API ani maszynerii auth. Zwykły widok @csrf_exempt obok twoich endpointów DRF to najczystszy wzorzec.

Opcja B: DRF z surowym dostępem

Jeśli naprawdę chcesz DRF, czytaj request.body (bazowe żądanie Django) zamiast request.data:

from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import AllowAny

@api_view(['POST'])
@authentication_classes([])         # brak auth dla webhooków
@permission_classes([AllowAny])     # dostawca już uwierzytelnia podpisem
def stripe_webhook(request):
    payload = request._request.body  # bazowe żądanie Django, surowe bajty
    signature = request.META.get('HTTP_STRIPE_SIGNATURE', '')
    # ...zweryfikuj i obsłuż

Pośrednictwo request._request jest niezgrabne, ale to oficjalna furtka, gdy musisz ominąć parsowanie DRF.

Skonfiguruj tunel

  1. Uruchom serwer deweloperski Django: python manage.py runserver 8000.
  2. Dodaj hostname tunelu do ALLOWED_HOSTS w settings.py. Najprościej ['*'] w lokalnym devie, albo konkretna domena tunelu, jeśli wolisz.
  3. npx portpreview 8000 w osobnym terminalu.
  4. Wklej URL tunelu plus ścieżkę webhooka do panelu dostawcy.
  5. Wyzwól zdarzenie testowe i patrz, jak trafia do twojego widoku.

Zapomnienie ALLOWED_HOSTS daje ogólny Django-400 bez oczywistej przyczyny. Jeśli pierwsza dostawa webhooka zwraca 400, a nie widzisz logów widoku, to właśnie dlatego.

Częste pułapki

Dwukrotny odczyt ciała

Jak każdy framework, Django daje ciało jako strumień, który da się odczytać tylko raz. Jeśli middleware lub wrapper logujący już dotknął request.body, twój kod widoku widzi pusty obiekt bytes. Wypisz len(request.body) dla sprawdzenia.

Strefy czasowe i tolerancja timestampu

Jeśli porównujesz podpisany timestamp z datetime.now(), używaj timezone.now() lub porównuj w UTC. Porównania naiwnych datetime zawodzą okresowo przy zmianach czasu letniego.

Widoki async

Django 4+ wspiera widoki async def. Handlery webhooków mogą być async, ale pamiętaj, że SDK weryfikacji podpisu (Stripe Python itd.) są synchroniczne. Uruchamiaj je przez asgiref.sync_to_async albo utrzymaj cały widok synchroniczny.

Uwagi o wdrożeniu produkcyjnym

Gdy miniesz testy lokalne, ten sam widok działa na produkcji. Gunicorn, uWSGI, Daphne — żaden nie dotyka request.body, zanim zobaczy go twój widok. Twój reverse proxy może (np. nginx z włączonym proxy_buffering), ale to kwestia buforowania, nie modyfikacji ciała.

O leżącej u podstaw teorii podpisów zobacz przewodnik po weryfikacji podpisów. Dla wzorców specyficznych dla Stripe stronę panelu opisuje przewodnik po lokalnym testowaniu Stripe. Dołącz do listy oczekujących PortPreview po tunele, które dobrze współpracują z serwerami deweloperskimi Pythona.

Najczęściej zadawane pytania

Dlaczego Django zwraca 403 na każdy POST webhooka?
Middleware CSRF Django odrzuca POST-y bez ważnego tokenu CSRF. Dostawcy webhooków go nie wysyłają. Dodaj @csrf_exempt do widoku webhooka (tylko tego widoku), by ominąć sprawdzenie, i zamiast tego weryfikuj podpis.
Widoki webhooków powinny być w zwykłym Django czy DRF?
Zwykłe Django jest prostsze dla webhooków. Parsery DRF konsumują ciało, zanim zobaczy je twój widok, co psuje weryfikację podpisu. Jeśli zostajesz przy DRF, czytaj request._request.body, by uzyskać surowe bajty.
Jakie ustawienie ALLOWED_HOSTS jest potrzebne dla tuneli w Django?
Dodaj domenę tunelu do ALLOWED_HOSTS w settings.py. Podczas lokalnego developmentu działa ['*']; dla surowszych konfiguracji wypisz konkretny hostname tunelu. Bez tego Django zwraca 400 bez jasnego komunikatu w logach.