Tất cả bài viết
DjangoPythonwebhook debugginglocal testing

Webhook Django: vượt CSRF vào tới handler

Mặc định, Django sẽ từ chối mọi POST webhook bằng 403. Thủ phạm là middleware CSRF đang làm việc của nó — bảo vệ chống cross-site request forgery — chỉ có điều nhà cung cấp không phải trình duyệt và không mang cookie phiên hay token CSRF. Cách sửa là một decorator và một dòng nhận thức về việc truy cập body của yêu cầu.

Bỏ qua CSRF cho các view webhook

Cách của 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            # byte thô, chưa đụng tới
    signature = request.headers.get('Stripe-Signature', '')

    # Xác minh chữ ký (mã thật dùng 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)

Decorator @csrf_exempt gỡ kiểm tra của middleware chỉ cho view này — các form khác của bạn vẫn được bảo vệ. request.body trả về byte thô như khi đến, đúng thứ mà việc xác minh chữ ký cần.

Nếu bạn dùng Django REST Framework

DRF bọc yêu cầu trong object Request riêng và chạy parser theo content-type. Đến lúc mã view của bạn đọc request.data, parser JSON đã tiêu thụ body. Có hai lối thoát:

Lựa chọn A: giữ là view Django thuần

Handler webhook không cần parser của DRF, API duyệt được hay bộ máy auth. Một view @csrf_exempt thuần đặt cạnh các endpoint DRF là khuôn mẫu sạch nhất.

Lựa chọn B: dùng DRF với truy cập thô

Nếu bạn thực sự muốn DRF, hãy đọc request.body (yêu cầu Django bên dưới) thay vì request.data:

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

@api_view(['POST'])
@authentication_classes([])         # không auth cho webhook
@permission_classes([AllowAny])     # nhà cung cấp đã xác thực bằng chữ ký
def stripe_webhook(request):
    payload = request._request.body  # yêu cầu Django bên dưới, byte thô
    signature = request.META.get('HTTP_STRIPE_SIGNATURE', '')
    # ...xác minh và xử lý

Việc gián tiếp request._request hơi vụng nhưng là lối thoát chính thức khi bạn cần né việc parse của DRF.

Thiết lập tunnel

  1. Chạy máy chủ dev Django: python manage.py runserver 8000.
  2. Thêm hostname của tunnel vào ALLOWED_HOSTS trong settings.py. Dễ nhất là ['*'] khi dev cục bộ, hoặc một domain tunnel cụ thể nếu bạn thích.
  3. npx portpreview 8000 trong một terminal khác.
  4. Dán URL tunnel cùng đường dẫn webhook vào bảng điều khiển nhà cung cấp.
  5. Kích hoạt một sự kiện thử nghiệm và xem nó rơi vào view của bạn.

Quên ALLOWED_HOSTS tạo ra một lỗi Django 400 chung chung không rõ nguyên nhân. Nếu lần gửi webhook đầu tiên trả về 400 và bạn không thấy log của view, đó là lý do.

Những cái bẫy thường gặp

Đọc body hai lần

Như mọi framework, Django đưa body dưới dạng stream chỉ đọc được một lần. Nếu middleware hay wrapper logging đã đụng request.body, mã view của bạn thấy một object bytes rỗng. In len(request.body) để kiểm tra.

Múi giờ và dung sai timestamp

Nếu bạn so timestamp đã ký với datetime.now(), hãy dùng timezone.now() hoặc so trong UTC. So sánh datetime naive thất bại chập chờn khi đổi giờ mùa hè.

View async

Django 4+ hỗ trợ view async def. Handler webhook có thể async, nhưng lưu ý các SDK xác minh chữ ký (Stripe Python, v.v.) là đồng bộ. Chạy chúng bằng asgiref.sync_to_async hoặc giữ cả view đồng bộ.

Ghi chú triển khai production

Một khi vượt qua kiểm thử cục bộ, cùng view đó chạy ở production. Gunicorn, uWSGI, Daphne — không cái nào đụng request.body trước khi view của bạn thấy nó. Reverse proxy của bạn có thể (ví dụ nginx bật proxy_buffering), nhưng đó là chuyện đệm, không phải sửa đổi body.

Về lý thuyết chữ ký nền tảng, xem hướng dẫn xác minh chữ ký. Với các khuôn mẫu riêng cho Stripe, phía bảng điều khiển được hướng dẫn kiểm thử Stripe cục bộ đề cập. Tham gia danh sách chờ của PortPreview để có tunnel hợp với máy chủ dev Python.

Câu hỏi thường gặp

Vì sao Django trả về 403 cho mọi POST webhook?
Middleware CSRF của Django từ chối các POST không có token CSRF hợp lệ. Nhà cung cấp webhook không gửi token. Thêm @csrf_exempt vào view webhook của bạn (chỉ view đó) để bỏ qua kiểm tra, rồi xác minh chữ ký thay thế.
View webhook nên là Django thuần hay DRF?
Django thuần đơn giản hơn cho webhook. Parser của DRF tiêu thụ body trước khi view thấy, làm hỏng việc xác minh chữ ký. Nếu giữ DRF, hãy đọc request._request.body để lấy byte thô.
Cần thiết lập ALLOWED_HOSTS nào cho tunnel trong Django?
Thêm domain tunnel vào ALLOWED_HOSTS trong settings.py. Khi dev cục bộ, ['*'] hoạt động; với cấu hình chặt hơn hãy liệt kê hostname tunnel cụ thể. Thiếu nó, Django trả về 400 mà không có thông điệp log rõ ràng.