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
- Chạy máy chủ dev Django:
python manage.py runserver 8000. - Thêm hostname của tunnel vào
ALLOWED_HOSTStrongsettings.py. Dễ nhất là['*']khi dev cục bộ, hoặc một domain tunnel cụ thể nếu bạn thích. npx portpreview 8000trong một terminal khác.- Dán URL tunnel cùng đường dẫn webhook vào bảng điều khiển nhà cung cấp.
- 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.