Django AJAX POST请求中的CSRF保护与数据持久化实践

心靈之曲
发布: 2025-07-30 13:26:01
原创
772人浏览过

django ajax post请求中的csrf保护与数据持久化实践

本文深入探讨了在Django项目中,通过AJAX POST请求更新数据时遇到的常见问题——数据无法持久化。核心原因在于Django内置的跨站请求伪造(CSRF)保护机制。文章详细阐述了CSRF的工作原理,并提供了客户端(JavaScript Fetch API)和服务器端(Django视图)的完整解决方案,包括如何安全地获取并发送CSRF令牌,以及后端视图的正确处理方式,确保数据更新操作的顺利进行与持久化。

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中提取这个令牌。

来画数字人直播
来画数字人直播

来画数字人自动化直播,无需请真人主播,即可实现24小时直播,无缝衔接各大直播平台。

来画数字人直播 0
查看详情 来画数字人直播
// 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 %}

<div class="container d-flex align-items-center justify-content-center" style="min-height: 100vh;">
    <div class="text-center">
        <h1>{{ room_bills.title }}</h1>
        <p>Due: {{ room_bills.due }}</p>

        <div class="col-3">
            <h3>Paid Members: </h3>
            <ul>
                {% for submission in submissions %}
                <li>
                    <a href="proof/{{ submission.user.id }}" target="_blank">{{ submission.user.username }} {{ submission.text }}</a>
                    <form>
                        <label for="status">Status:</label>
                        <select name="status" id="{{ submission.id }}" onblur="postStatus('{{ submission.id }}')">
                            <option value="P" {% if submission.status == 'P' %}selected{% endif %}>Pending</option>
                            <option value="A" {% if submission.status == 'A' %}selected{% endif %}>Accepted</option>
                            <option value="R" {% if submission.status == 'R' %}selected{% endif %}>Rejected</option>
                        </select>
                    </form>
                </li>
                {% endfor %}
            </ul>
        </div>

        <div class="col-3">
            <h3>Did not pay members: </h3>
            <ul>
                {% for user in did_not_submit %}
                    <li>{{ user.username }}</li>
                {% endfor %}
            </ul>
        </div>
    </div>
</div>

<script>
    // 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();
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }

    function postStatus(submissionId) {
        const selectElement = document.getElementById(submissionId);
        const status = selectElement.value;
        const csrftoken = getCookie('csrftoken'); // 获取CSRF令牌

        fetch(`/room/{{ room.join_code }}/bills/{{ bills.slug }}/status/`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRFToken': csrftoken // 添加CSRF令牌头部
            },
            body: JSON.stringify({
                // 注意:这里将 "submissionId" 改为 "submission_id" 以匹配后端Python的命名习惯
                "submission_id": submissionId,
                "status": status
            })
        })
        .then(response => {
            // 检查响应状态码,例如200 OK
            if (!response.ok) {
                // 如果不是成功的响应,抛出错误
                return response.json().then(errorData => {
                    throw new Error(`HTTP error! Status: ${response.status}, Message: ${JSON.stringify(errorData)}`);
                });
            }
            return response.json(); // 解析JSON响应
        })
        .then(data => {
            // 请求成功处理
            if (data.success) {
                console.log('Status updated successfully:', data);
                // 可以在这里添加UI反馈,例如显示成功消息
            } else {
                console.error('Status update failed:', data);
                // 可以在这里添加UI反馈,例如显示错误消息
            }
        })
        .catch(error => {
            // 请求失败或网络错误处理
            console.error('Error updating status:', error);
            // 可以在这里添加UI反馈,例如显示错误消息
        });
    }

    // 页面加载后,根据当前submission.status设置select的选中项
    document.addEventListener('DOMContentLoaded', function() {
        {% for submission in submissions %}
            const selectElement = document.getElementById('{{ submission.id }}');
            if (selectElement) {
                selectElement.value = '{{ submission.status }}';
            }
        {% endfor %}
    });
</script>

{% endblock %}
登录后复制

代码改进说明:

  1. CSRF令牌获取与传递:
    • 新增 getCookie 函数用于从 document.cookie 中提取 csrftoken。
    • 在 postStatus 函数中,调用 getCookie('csrftoken') 获取令牌。
    • 在 fetch 请求的 headers 中,添加 'X-CSRFToken': csrftoken。
  2. 数据字段命名一致性: 将 body 中的 "submissionId" 改为 "submission_id",使其与后端 views.py 中 data["submission_id"] 的命名保持一致,避免因键名不匹配导致数据无法正确解析。
  3. 下拉菜单默认选中项: 在 <option> 标签中添加 {% if submission.status == 'P' %}selected{% endif %} 等,确保页面加载时,下拉菜单显示的是当前实际的状态,而不是默认的“Pending”。
  4. 错误处理与用户反馈: 为 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)
登录后复制

后端视图改进说明:

  1. 移除@csrf_exempt: 这是最关键的一步。一旦前端正确发送CSRF令牌,后端就不再需要豁免CSRF检查,从而增强了安全性。
  2. 使用@require_POST: 这是一个推荐的安全实践,确保该视图只响应POST请求,对于其他HTTP方法(如GET)将返回405 Method Not Allowed。
  3. 健壮性改进:
    • 使用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/<str:room_id>/bills/<str:bills_slug>/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应用安全可靠。

以上就是Django AJAX POST请求中的CSRF保护与数据持久化实践的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
热门推荐
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号