Django callback 报 Forbidden Referer checking failed
结论
第三方支付平台的服务器端回调通常不会发送 Referer,因此支付 webhook 不应依赖 Django 的 CSRF 或 Referer 校验。建议这样处理:
- 只为实际接收回调的 view 添加
@csrf_exempt。 - 确认
/callback/的 URL 指向的正是这个已豁免的 view。 - 使用支付平台提供的签名机制验证请求。关闭 CSRF 后,不能直接信任请求内容。
- 如果添加
@csrf_exempt后仍然出现同样的错误,通常是因为装饰器没有作用于最终执行的 view,或者其他中间件、DRF 认证类仍在执行 CSRF 校验。
Django 的标准配置名是 CSRF_TRUSTED_ORIGINS,不是 CSRF_ALLOWED_ORIGIN。即使正确配置了可信来源,也无法解决请求完全没有 Referer 的问题。
为什么会出现这个错误
Django 的 CSRF 防护主要用于浏览器中的 Cookie 会话。对于 HTTPS 请求,CsrfViewMiddleware 可能检查 Origin 或 Referer,防止恶意网站利用用户现有的登录状态发起跨站请求。
支付平台回调的请求方式不同。它通常由支付平台的服务器直接向你的服务器发送 POST 请求,不经过浏览器,所以很可能不包含以下请求头或 token:
Referer
Origin
CSRFToken
这是正常现象。CORS 配置也无法解决这个问题,因为 CORS 是由浏览器执行的跨域限制,对服务器之间的 webhook 请求没有实际作用。
正确配置函数视图
装饰器必须直接作用于 urls.py 最终引用的函数:
# views.py
import json
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def callback(request):
if request.method != "POST":
return HttpResponse(status=405)
try:
payload = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "invalid JSON"}, status=400)
# 先验证支付平台签名,再处理并保存交易
# verify_signature(request.headers, request.body)
return JsonResponse({"received": True})
URL 必须引用同一个函数:
# urls.py
from django.urls import path
from .views import callback
urlpatterns = [
path("callback/", callback, name="payment-callback"),
]
常见问题是装饰了一个函数,但 URL 实际指向另一个包装函数或同名 view:
# callback 被豁免,但 URL 实际指向 payment_callback
path("callback/", payment_callback)
此时,@csrf_exempt 不会生效。
类视图的处理方式
如果使用 class-based view,应对 dispatch 进行豁免:
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
@method_decorator(csrf_exempt, name="dispatch")
class PaymentCallbackView(View):
def post(self, request, *args, **kwargs):
return JsonResponse({"received": True})
对应的 URL 配置如下:
from django.urls import path
from .views import PaymentCallbackView
urlpatterns = [
path(
"callback/",
PaymentCallbackView.as_view(),
name="payment-callback",
),
]
在部分组合方式下,只装饰 post() 方法可能无法避开中间件检查,因此装饰 dispatch 更稳妥。
使用 Django REST Framework 时
如果 endpoint 使用 Django REST Framework,还需要检查认证配置。SessionAuthentication 会执行 CSRF 校验,仅在方法上添加 @csrf_exempt 不一定能得到预期结果。
支付 webhook 一般不应使用浏览器 Session 认证,可以为该接口单独设置认证方式:
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
class PaymentCallbackView(APIView):
authentication_classes = []
permission_classes = [AllowAny]
def post(self, request):
# 必须在这里验证支付平台签名
return Response({"received": True})
这不代表接口可以不做认证。AllowAny 只是取消 DRF 对用户登录状态的要求,真正的可信校验仍应由支付平台的 webhook 签名、共享密钥、证书或其他官方验证机制完成。
仍然报错时如何排查
1. 确认错误由哪个组件产生
Django 原生的 CSRF 错误通常由 CsrfViewMiddleware 返回。检查 MIDDLEWARE 中是否存在重复项,以及项目是否添加了自定义的 CSRF 或 Referer 检查中间件:
MIDDLEWARE = [
# ...
"django.middleware.csrf.CsrfViewMiddleware",
# ...
]
不要因为一个 callback 就删除全局的 CsrfViewMiddleware。其他浏览器表单和后台页面仍然需要 CSRF 防护,只应豁免明确的 webhook endpoint。
2. 检查最终解析到的 view
可以在 Django shell 中查看 URL 的解析结果:
from django.urls import resolve
match = resolve("/callback/")
print(match.func)
print(getattr(match.func, "csrf_exempt", False))
对于正确豁免的函数视图,第二行通常会输出:
True
如果输出 False,说明 URL 最终指向的 callable 没有被正确豁免。
3. 检查路径和尾部斜杠
确认支付平台请求的地址是:
/callback/
而不是:
/callback
如果请求发生重定向,第三方平台可能无法正确处理,POST 请求体也可能受到影响。支付平台后台填写的地址最好与 Django URL 完全一致,不要依赖 APPEND_SLASH 自动纠正。
4. 直接模拟无 Referer 请求
可以发送一个不包含 Referer 和 CSRF token 的请求:
curl -i \
-X POST \
-H 'Content-Type: application/json' \
--data '{"transaction_id":"test-001"}' \
https://example.com/callback/
如果 @csrf_exempt 已正确作用于最终 view,这个请求不应再因 CSRF Referer 检查返回 403。不过,它仍可能被签名验证、代理规则或其他业务校验拒绝。
5. 检查反向代理和外围安全层
如果已经确认 Django 中的 csrf_exempt=True,但错误依然存在,还需要检查:
- Nginx、Apache 或网关是否限制了请求头。
- 是否存在多个 Django 服务实例,其中部分实例尚未更新。
- 请求是否实际到达了另一个应用或旧版本部署。
- 是否安装了额外的安全中间件。
- 请求是否经过内部转发,进入了另一个仍启用 CSRF 校验的 view。
CSRF_TRUSTED_ORIGINS 何时使用
Django 的正确配置示例如下:
CSRF_TRUSTED_ORIGINS = [
"https://payments.example.com",
]
这项配置适合允许受信任的浏览器来源提交需要 CSRF 校验的请求。它不是支付 webhook 的身份验证机制,也无法保证第三方请求一定会包含 Origin 或 Referer。
不要为了消除错误而加入范围过大的通配域名,更不要把所有未知来源都设为可信。
支付回调必须补上的安全措施
关闭 CSRF 后,理论上任何人都可以向 /callback/ 发送请求,因此必须验证支付平台提供的签名。例如:
import hashlib
import hmac
from django.conf import settings
def verify_signature(raw_body: bytes, supplied_signature: str) -> bool:
expected_signature = hmac.new(
settings.PAYMENT_WEBHOOK_SECRET.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected_signature, supplied_signature)
实际使用的签名算法、请求头名称、时间戳处理方式和编码方式,必须以支付平台的官方文档为准,不能自行假设。验证签名时通常应使用原始 request.body,避免 JSON 重新序列化后内容发生变化,造成签名不一致。
还需要处理以下事项:
- 使用交易号或事件 ID 建立唯一约束,保证幂等性。
- 在数据库事务中更新支付状态。
- 校验金额、币种、订单号和商户号。
- 防止旧请求被重放,必要时验证时间戳。
- 不要记录完整卡号、密钥、签名原文等敏感信息。
- 成功保存后,返回支付平台要求的 2xx 响应。
- 不要仅凭 callback 中的
status=paid就认定付款成功。必要时调用支付平台的查询接口进行二次确认。
问题的解决办法不是把第三方域名加入 CSRF 白名单,而是让实际接收请求的 callback view 跳过浏览器 CSRF 检查,再用支付平台提供的签名机制验证请求。
备注:内容仅供参考。
