如何在 django 中接收支付网关的 POST 数据?
结论
PHP 示例读取的是 $_POST。在 Django 中,对应的写法是 request.POST,请求方法也应判断为 POST:
if request.method == "POST":
payment_id = request.POST.get("paymentid")
request.GET 只包含 URL 中的查询参数,例如 ?paymentid=123,不会读取 POST 请求体。因此,当前的 request.method == "GET" 和 request.GET['paymentid'] 与网关示例并不匹配。
基本处理方式
可以为支付网关的返回地址单独编写一个视图:
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.http import require_POST
@require_POST
def payment_callback(request):
error_code = request.POST.get("Error", "")
error_text = request.POST.get("ErrorText", "")
payment_id = request.POST.get("paymentid", "")
if not payment_id:
return HttpResponseBadRequest("Missing paymentid")
# 根据网关规则校验签名、金额、订单号和支付状态
# 校验成功后再更新本地订单
return HttpResponse("OK")
URL 配置如下:
from django.urls import path
from . import views
urlpatterns = [
path("payments/callback/", views.payment_callback, name="payment_callback"),
]
这里使用 request.POST.get(),而不是:
request.POST["paymentid"]
两种写法都能读取字段,但如果字段不存在,方括号写法会抛出 MultiValueDictKeyError。.get() 会返回 None,也可以返回指定的默认值,因此更适合接收外部系统发送的数据:
payment_id = request.POST.get("paymentid", "")
同时检查 GET 和 POST
调试时,可以先确认网关实际发送的是哪种请求:
from django.http import JsonResponse
def payment_callback(request):
return JsonResponse({
"method": request.method,
"content_type": request.content_type,
"get": request.GET.dict(),
"post": request.POST.dict(),
})
这段代码只适合临时测试。生产环境中,不要把支付参数、签名、银行卡信息或其他敏感数据原样返回给客户端。
如果网关有时发送 POST,有时通过查询字符串跳转,可以明确区分两种情况:
def payment_callback(request):
if request.method == "POST":
payment_id = request.POST.get("paymentid")
elif request.method == "GET":
payment_id = request.GET.get("paymentid")
else:
payment_id = None
但不要为了所谓的兼容而直接接受两种方式。具体应以支付网关文档和实际请求为准。
为什么浏览器发生了跳转,却不一定是 GET
“重定向到指定 URL”并不意味着请求一定是 GET。支付网关可能采用以下任一种方式:
- 返回
302响应,让浏览器通过 GET 访问返回地址; - 输出隐藏表单,再通过 JavaScript 自动提交 POST;
- 由网关服务器直接向回调地址发送 POST;
- 让浏览器跳转到结果页,同时另行发送服务器端通知。
PHP 示例使用了 $_POST,说明该示例预期接收表单格式的 POST。不过,仍需查看网关如何定义“返回地址”和“通知地址”,因为两者可能不是同一个接口。
POST 数据为空时需要检查什么
即使请求方法是 POST,request.POST 也可能为空。常见原因是网关发送的数据不是表单格式。
request.POST 主要解析以下内容类型:
application/x-www-form-urlencoded
multipart/form-data
如果网关发送的是 JSON,例如:
Content-Type: application/json
就需要读取并解析 request.body:
import json
from django.http import HttpResponseBadRequest, JsonResponse
def payment_callback(request):
if request.method != "POST":
return HttpResponseBadRequest("POST required")
try:
payload = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponseBadRequest("Invalid JSON")
payment_id = payload.get("paymentid")
error_code = payload.get("Error")
error_text = payload.get("ErrorText")
return JsonResponse({"received": bool(payment_id)})
排查时至少要确认以下内容:
request.method
request.content_type
request.GET
request.POST
request.body
记录 request.body 时要格外谨慎,避免把支付凭证或个人信息写入日志。
第三方 POST 与 CSRF
支付网关是第三方系统,通常无法携带 Django 页面生成的 CSRF token。如果请求确实被 Django 的 CSRF 中间件拒绝,可以只对专用回调接口使用 csrf_exempt:
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def payment_callback(request):
payment_id = request.POST.get("paymentid")
# 必须按照网关文档验证签名或消息认证信息
# 验证成功后才能更新订单状态
return HttpResponse("OK")
不要关闭整个站点的 CSRF 防护。还要注意,csrf_exempt 只是允许请求进入视图,不能证明请求来自支付网关。
支付回调不能只相信 paymentid
任何人都可以构造这样的 POST 请求:
paymentid=123
收到数据后,还要按照网关协议完成安全校验,通常包括:
- 校验签名、摘要或消息认证码;
- 核对订单号、支付金额和币种;
- 必要时调用网关的服务端查询接口确认交易状态;
- 防止重复处理同一个通知;
- 只允许合法的订单状态转换;
- 在数据库事务中更新支付记录;
- 按照网关要求返回确认响应。
应将“用户浏览器跳转到支付结果页”和“确认订单已付款”分开处理。页面跳转可以用来展示结果,但订单是否支付成功,应以通过签名验证或服务端查询确认的通知为准。
备注:内容仅供参考。
