
1. 问题背景与现象分析
在开发基于Django的Web应用时,我们经常需要通过前端JavaScript(如Fetch API或XMLHttpRequest)向后端发送异步请求来更新数据,例如更改某个状态或提交表单。一个常见的问题是,尽管前端代码看似正确发送了POST请求,并且后端视图也执行了数据保存操作(如instance.save()),但刷新页面后数据却回到了初始状态。这通常表现为数据未能成功持久化到数据库中。
例如,在预算管理应用中,尝试通过下拉菜单更改账单提交状态(Pending, Accepted, Rejected),但每次页面刷新后,状态都恢复为“Pending”。这表明后端可能并未真正接收到请求数据,或者在处理过程中被某个安全机制拦截。
2. 理解Django的CSRF保护机制
Django默认开启了强大的跨站请求伪造(CSRF)保护机制。CSRF是一种恶意攻击,攻击者诱导用户在已登录状态下访问一个恶意网站,该网站向用户已登录的合法网站发送伪造的请求,利用用户的会话权限执行非法操作。
为了防范CSRF攻击,Django要求所有非GET、HEAD、OPTIONS、TRACE的请求(即会改变服务器状态的请求,如POST、PUT、DELETE)必须携带一个有效的CSRF令牌。这个令牌在用户首次访问页面时由Django生成并嵌入到HTML中(通常是隐藏的表单字段或Cookie中),并在后续的POST请求中被提交回服务器进行验证。如果请求中缺少CSRF令牌,或者令牌无效,Django的CSRF中间件会阻止该请求,导致数据无法到达视图函数,从而无法保存。
虽然可以使用@csrf_exempt装饰器来豁免某个视图的CSRF检查,但这会降低应用的安全性,通常不推荐在生产环境中使用,除非有非常明确的理由和额外的安全措施。最佳实践是始终在POST请求中包含CSRF令牌。
3. 解决方案:正确传递CSRF令牌
解决数据无法持久化问题的核心在于确保AJAX POST请求能够正确地携带CSRF令牌。
3.1 获取CSRF令牌
Django将CSRF令牌存储在一个名为csrftoken的Cookie中。在JavaScript中,我们需要编写一个辅助函数来从浏览器Cookie中提取这个令牌。
// getCookie函数用于从document.cookie中获取指定名称的Cookie值
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}3.2 在Fetch请求中添加CSRF头部
获取到CSRF令牌后,需要将其作为X-CSRFToken头部添加到Fetch请求的headers中。
{% extends "base/room_home.html" %}
{% block content %}
{{ room_bills.title }}
Due: {{ room_bills.due }}
Paid Members:
{% for submission in submissions %}
-
{{ submission.user.username }} {{ submission.text }}
{% endfor %}
Did not pay members:
{% for user in did_not_submit %}
- {{ user.username }}
{% endfor %}
{% endblock %}代码改进说明:
-
CSRF令牌获取与传递:
- 新增 getCookie 函数用于从 document.cookie 中提取 csrftoken。
- 在 postStatus 函数中,调用 getCookie('csrftoken') 获取令牌。
- 在 fetch 请求的 headers 中,添加 'X-CSRFToken': csrftoken。
- 数据字段命名一致性: 将 body 中的 "submissionId" 改为 "submission_id",使其与后端 views.py 中 data["submission_id"] 的命名保持一致,避免因键名不匹配导致数据无法正确解析。
- 下拉菜单默认选中项: 在
- 错误处理与用户反馈: 为 fetch 请求添加 .then().catch() 链,以便更好地处理请求成功、失败或网络错误,并提供控制台日志输出。在实际应用中,应替换为更友好的用户界面反馈。
3.3 后端视图处理
在后端,如果前端已经正确发送了CSRF令牌,那么您的Django视图就不需要使用@csrf_exempt装饰器。实际上,为了安全起见,我们应该移除它,让Django的CSRF中间件发挥作用。
import json
from django.http import JsonResponse
from django.views.decorators.http import require_POST # 推荐使用
# from django.views.decorators.csrf import csrf_exempt # 移除此行
from .models import Submission # 假设您的模型在这里
@require_POST # 确保只接受POST请求
# @csrf_exempt # 移除此装饰器,让CSRF中间件处理
def remark_proof_api(request, room_id, bills_slug):
# Django的CSRF中间件在请求到达这里之前已经验证了令牌
try:
data = json.loads(request.body.decode("utf-8"))
submission_id = data.get("submission_id") # 使用.get()防止KeyError
status = data.get("status")
if not submission_id or not status:
return JsonResponse({"success": False, "message": "Missing submission_id or status"}, status=400)
sub = Submission.objects.get(id=int(submission_id))
sub.status = status
sub.save()
return JsonResponse({"success": True, "message": "Status updated successfully"})
except Submission.DoesNotExist:
return JsonResponse({"success": False, "message": "Submission not found"}, status=404)
except json.JSONDecodeError:
return JsonResponse({"success": False, "message": "Invalid JSON"}, status=400)
except Exception as e:
# 记录详细错误以便调试
print(f"Error updating submission status: {e}")
return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}, status=500)后端视图改进说明:
- 移除@csrf_exempt: 这是最关键的一步。一旦前端正确发送CSRF令牌,后端就不再需要豁免CSRF检查,从而增强了安全性。
- 使用@require_POST: 这是一个推荐的安全实践,确保该视图只响应POST请求,对于其他HTTP方法(如GET)将返回405 Method Not Allowed。
-
健壮性改进:
- 使用data.get("key")代替data["key"],避免在键不存在时抛出KeyError。
- 添加try-except块来捕获潜在的错误,如Submission.DoesNotExist、json.JSONDecodeError或其他通用异常,并返回有意义的错误响应,提高API的健壮性。
- 在发生错误时,返回适当的HTTP状态码(如400 Bad Request, 404 Not Found, 500 Internal Server Error)。
3.4 URL配置
urls.py中的配置保持不变,因为它正确地映射了URL路径到视图函数。
# urls.py
from django.urls import path
from . import views
urlpatterns = [
# ... 其他URL模式
path('room//bills//status/', views.remark_proof_api, name='remark-proof'),
] 4. 总结与注意事项
通过以上步骤,您已经成功地在Django AJAX POST请求中集成了CSRF保护,并解决了数据无法持久化的问题。
- CSRF令牌的重要性: 始终优先考虑在AJAX POST请求中包含CSRF令牌,而不是简单地豁免视图的CSRF检查。这是Web应用安全的基本要求。
- 命名一致性: 前后端数据字段的命名(例如JavaScript中的submissionId与Python中的submission_id)必须保持一致,否则后端无法正确解析数据。
- 错误处理: 在前端和后端都实现健壮的错误处理机制。前端的.catch()块和后端的try-except块对于调试和提供用户反馈至关重要。利用浏览器开发者工具(Network Tab)检查请求和响应,可以帮助快速定位问题。
- 用户体验: 在数据更新成功或失败后,考虑向用户提供即时反馈,例如通过消息提示或UI元素的动态更新。
- 安全性: 除了CSRF,还应关注其他安全方面,如输入验证、权限检查等,确保您的Django应用安全可靠。










