开箱即用时,Django 会用 403 拒绝每一个 webhook POST。罪魁祸首是 CSRF 中间件在尽职——防御跨站请求伪造——只不过服务商不是浏览器,不带会话 cookie 或 CSRF 令牌。修复办法是一个装饰器,外加对请求体访问的一行认知。
为 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 解析器已经消费了请求体。两条出路:
方案 A:保留为普通 Django 视图
webhook 处理器不需要 DRF 的解析器、可浏览 API 或认证机制。在你的 DRF 端点旁放一个普通的 @csrf_exempt 视图是最干净的模式。
方案 B:用 DRF 但做原始访问
如果你真的想用 DRF,请读取 request.body(底层的 Django 请求)而不是 request.data:
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。 - 把隧道网址加上你的 webhook 路径粘贴到服务商控制台。
- 触发一个测试事件,看它落进你的视图。
忘了 ALLOWED_HOSTS 会产生一个没有明显原因的通用 Django 400。如果你第一次 webhook 投递返回 400 且看不到视图日志,原因就在这。
常见陷阱
把请求体读两次
和每个框架一样,Django 把请求体作为只能读一次的流给你。如果中间件或日志包装器已经碰过 request.body,你的视图代码会看到一个空字节对象。打印 len(request.body) 做个健全性检查。
时区与时间戳容差
如果你把签名时间戳和 datetime.now() 比较,确保使用 timezone.now() 或在 UTC 下比较。naive datetime 比较会在夏令时切换时间歇性失败。
异步视图
Django 4+ 支持 async def 视图。webhook 处理器可以是异步的,但要注意签名验证 SDK(Stripe Python 等)是同步的。用 asgiref.sync_to_async 运行它们,或把整个视图保持同步。
生产部署注意
过了本地测试,同一个视图在生产里也能用。Gunicorn、uWSGI、Daphne——它们都不会在你的视图看到之前碰 request.body。你的反向代理可能会(例如开启了 proxy_buffering 的 nginx),但那是缓冲问题,不是请求体修改。
关于底层签名理论,见签名验证指南。Stripe 专属模式的控制台一侧由Stripe 本地测试指南覆盖。加入 PortPreview 等候名单,获得与 Python 开发服务器配合良好的隧道。