资讯动态

Django中间件、CSRF与auth认证:从原理到实战的完整指南

发布时间:2026/10/13 9:07:43 来源:尧图企业网站定制
1. 从一个真实需求说起为什么中间件、CSRF和auth总是被一起提起做Django开发的人大概都有过这样的经历项目初期一切顺畅路由、视图、模板三件套跑通之后觉得自己已经掌握了这个框架。然后某一天需求突然变得复杂起来——需要给所有请求打日志、需要限制某些接口的访问频率、需要做统一的登录校验、需要防止表单被恶意提交。这时候你打开搜索引擎搜出来的答案里反复出现三个词中间件、跨站请求伪造、auth认证。这三个东西看起来是独立的模块实际上它们共同构成了Django请求处理链路中“安全与权限”这条主线。中间件是请求进出的大门CSRF是表单提交时的身份凭证auth是用户身份的判定依据。任何一个Web项目只要涉及到“谁能访问什么”这个问题就绕不开这三者的配合。我写这篇文章的出发点很简单网上讲Django中间件的文章很多讲CSRF的文章也很多讲auth认证的文章更多但把这三者串起来、讲清楚它们在实际项目中如何协同工作的内容却很少。很多初学者学完单个知识点后面对一个真实项目仍然不知道从哪里下手。所以这篇文章会从整体设计思路讲起然后逐个拆解核心细节再给出完整的实操流程最后把我在实际开发中踩过的坑和排查经验整理出来。这篇文章适合已经写过一些Django视图、对MTV模式有基本了解、但还没有系统处理过权限和安全问题的开发者。如果你正在做一个需要登录才能访问的后台系统或者正在为接口的重复提交问题头疼那接下来的内容应该能帮你省下不少查文档的时间。2. 整体设计思路三层防护如何各司其职2.1 请求生命周期中的三个关键卡点要理解这三者的关系最好的方式是先看一个请求从进入Django到返回响应中间到底经过了哪些环节。你可以把Django的处理流程想象成一条流水线请求从WSGI服务器进来先经过一层层的中间件然后到达URL路由再进入视图函数视图函数可能会调用ORM查数据库最后响应再沿着中间件反向传回去。中间件就是这条流水线上的关卡。每个中间件都有机会在请求到达视图之前做点什么也有机会在响应返回浏览器之前做点什么。这个“做点什么”可以是记录日志、可以是检查登录状态、可以是修改请求头、也可以是直接拦截请求返回一个错误响应。CSRF防护则是在表单提交这个具体场景下工作的。当浏览器提交一个POST表单时Django需要确认这个请求确实来自你自己的网站而不是某个第三方页面伪造的。它的做法是在渲染表单时嵌入一个隐藏的token提交时校验这个token是否匹配。这个校验动作通常由CSRF中间件来完成所以它和中间件是紧密绑定的。auth认证解决的是“这个请求是谁发的”这个问题。Django内置了一套完整的用户认证系统包括用户模型、密码哈希、登录登出、权限组等。当你用login()函数登录一个用户时Django会把用户ID存到session里后续请求通过session中间件把用户对象附加到request.user上。这样视图函数里就能直接判断当前用户是否已登录、是否有某个权限。2.2 为什么选择Django内置方案而不是自己造轮子有些开发者可能会想中间件我自己写一个装饰器不就行了CSRF我自己生成一个随机字符串存session不就行了auth我自己建一张用户表不就行了理论上都可以但实际项目中自己造轮子有几个绕不开的问题。第一是安全性CSRF token的生成和校验涉及到时序攻击防护、token轮换、SameSite Cookie策略等细节自己实现很容易留下漏洞。第二是兼容性Django的auth系统和admin后台、权限系统、session框架是深度耦合的自己另起炉灶会导致admin用不了、权限判断要重写。第三是维护成本内置方案有官方安全团队持续更新自己写的代码出了问题只能自己扛。所以我的建议很明确除非有非常特殊的业务需求否则中间件、CSRF、auth这三块都应该优先使用Django内置的方案把精力放在业务逻辑上。理解它们的原理是为了更好地配置和排查问题而不是为了替换它们。2.3 三者的协作关系与数据流向用一个具体的场景来说明三者的协作用户打开登录页面填写用户名密码点击提交。第一步GET请求到达登录页面视图Django渲染模板时CSRF中间件会自动在模板中插入一个隐藏的csrfmiddlewaretoken字段。这个token和当前session绑定存在服务器端。第二步用户提交表单POST请求带着这个token一起发送。请求先经过中间件栈CSRF中间件会取出请求中的token和session中的token进行比对。如果匹配请求继续往下走如果不匹配直接返回403错误。第三步请求到达登录视图视图调用authenticate()验证用户名密码验证通过后调用login()把用户信息写入session。这时候auth系统开始介入它会在session中记录用户ID和认证后端信息。第四步后续请求中session中间件根据session中的用户ID从数据库加载用户对象赋值给request.user。这样在任何视图里都可以通过request.user.is_authenticated判断用户是否登录。整个流程中中间件负责“放行或拦截”CSRF负责“验证请求来源”auth负责“识别用户身份”。三者各司其职又通过session这个共享存储串联在一起。3. 核心细节解析中间件、CSRF与auth的关键机制3.1 中间件的两种写法与执行顺序Django中间件有两种写法函数式和类式。函数式写法在较新版本的Django中已经不太推荐但理解它有助于明白中间件的本质。函数式中间件就是一个接收get_response可调用对象的函数返回一个接收request的函数。类式写法则是定义一个类实现__init__和__call__方法或者继承MiddlewareMixin。我个人的习惯是使用类式写法并继承MiddlewareMixin因为这样代码结构更清晰而且可以方便地定义process_request、process_response、process_view、process_exception等钩子方法。不过需要注意的是在新版Django中MiddlewareMixin的某些钩子已经不再被调用官方推荐直接实现__call__方法。中间件的执行顺序由settings.py中的MIDDLEWARE列表决定。请求阶段从上往下执行响应阶段从下往上执行。这个顺序非常关键因为如果认证中间件排在CSRF中间件后面那么未认证的请求会先被CSRF拦截而不是先跳转到登录页。通常的顺序是SecurityMiddleware、SessionMiddleware、CommonMiddleware、CsrfViewMiddleware、AuthenticationMiddleware、MessageMiddleware最后是自定义中间件。注意自定义中间件如果需要在请求阶段修改request.user必须放在AuthenticationMiddleware之后否则request.user还不存在。3.2 CSRF防护的三种token传递方式Django的CSRF防护默认只对POST、PUT、PATCH、DELETE这些“不安全”的方法生效GET、HEAD、OPTIONS、TRACE不受影响。token的传递有三种方式适用于不同的场景。第一种是表单隐藏字段这是最传统的方式。在模板中使用{% csrf_token %}标签Django会自动生成一个input typehidden namecsrfmiddlewaretoken value...。提交表单时这个字段会随表单一起发送。第二种是请求头传递适用于AJAX请求。Django会检查请求头中的X-CSRFToken字段。前端需要从Cookie中读取csrftoken的值然后设置到请求头中。这种方式在前后端分离的项目中很常见。第三种是Cookie传递但这种方式需要配合CSRF_COOKIE_HTTPONLY设置为False因为JavaScript需要读取Cookie的值。默认情况下Django会把CSRF token同时写入Cookie和模板上下文方便前端获取。在实际项目中我通常会在基础模板中放一段JavaScript代码统一从Cookie中读取token并设置到所有AJAX请求的头部。这样就不用在每个AJAX请求里手动添加了。3.3 auth认证系统的核心组件与扩展方式Django的auth系统由几个核心组件构成User模型、Group模型、Permission模型、认证后端、登录登出函数、权限装饰器。User模型是内置的字段包括username、password、email、first_name、last_name、is_active、is_staff、is_superuser、last_login、date_joined。实际项目中内置User模型的字段往往不够用比如需要手机号、头像、昵称等。这时候有两种扩展方式一种是创建一个Profile模型通过OneToOneField关联User另一种是自定义User模型继承AbstractUser或AbstractBaseUser。我的建议是如果项目刚开始毫不犹豫地使用自定义User模型继承AbstractUser添加需要的字段。因为Django官方文档明确说过项目中途更换User模型非常麻烦涉及到数据库迁移和外键关系。如果项目已经上线且用的是内置User模型那就用Profile方式扩展虽然多一次查询但胜在安全。认证后端决定了用户如何被验证。默认的ModelBackend使用用户名和密码验证。如果需要支持手机号登录、邮箱登录、或者第三方登录就需要自定义认证后端。自定义后端需要实现authenticate()和get_user()两个方法。权限系统方面Django为每个模型自动创建add、change、delete、view四种权限。可以通过user.has_perm()检查权限也可以用permission_required装饰器限制视图访问。对于更复杂的权限需求可以使用Group来批量分配权限或者引入第三方权限库。4. 实操过程从零搭建一个带完整认证与防护的后台系统4.1 项目初始化与自定义用户模型先创建一个新的Django项目和应用。假设项目名为mysite应用名为accounts。在settings.py中注册应用然后开始自定义用户模型。# accounts/models.py from django.contrib.auth.models import AbstractUser from django.db import models class CustomUser(AbstractUser): phone models.CharField(max_length20, blankTrue, verbose_name手机号) nickname models.CharField(max_length50, blankTrue, verbose_name昵称) avatar models.ImageField(upload_toavatars/, blankTrue, verbose_name头像) class Meta: verbose_name 用户 verbose_name_plural verbose_name def __str__(self): return self.username在settings.py中设置AUTH_USER_MODEL accounts.CustomUser。这一步必须在第一次迁移之前完成否则会报错。设置完成后执行makemigrations和migrate。注意如果已经执行过迁移再修改AUTH_USER_MODEL需要删除数据库和迁移文件重新来。所以这个决定要在项目最开始就做好。4.2 编写自定义中间件实现请求日志与访问控制接下来创建一个中间件用于记录请求日志和限制未登录用户的访问。在应用目录下新建middleware.py。# accounts/middleware.py import time import logging from django.utils.deprecation import MiddlewareMixin from django.shortcuts import redirect from django.urls import reverse logger logging.getLogger(django.request) class RequestLogMiddleware(MiddlewareMixin): def process_request(self, request): request.start_time time.time() def process_response(self, request, response): if hasattr(request, start_time): duration time.time() - request.start_time logger.info( f{request.method} {request.path} fstatus{response.status_code} fduration{duration:.3f}s fuser{request.user} ) return response class LoginRequiredMiddleware(MiddlewareMixin): EXEMPT_PATHS [/accounts/login/, /accounts/register/, /admin/login/] def process_request(self, request): if request.path in self.EXEMPT_PATHS: return None if request.path.startswith(/static/) or request.path.startswith(/media/): return None if not request.user.is_authenticated: return redirect(reverse(accounts:login) ?next request.path) return None在settings.py的MIDDLEWARE列表末尾添加这两个中间件。注意LoginRequiredMiddleware必须放在AuthenticationMiddleware之后因为需要用到request.user。4.3 配置CSRF与AJAX请求的token传递在settings.py中确认CSRF相关配置。默认情况下CsrfViewMiddleware已经在MIDDLEWARE中django.template.context_processors.csrf也在模板上下文中。需要额外配置的是Cookie相关选项。CSRF_COOKIE_HTTPONLY False # 允许JS读取cookie中的token CSRF_COOKIE_SAMESITE Lax CSRF_TRUSTED_ORIGINS [http://localhost:8000, http://127.0.0.1:8000]在基础模板中添加一段JavaScript代码统一处理AJAX请求的CSRF token。// static/js/csrf.js 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; } const csrftoken getCookie(csrftoken); // 如果使用jQuery $.ajaxSetup({ beforeSend: function(xhr, settings) { if (!/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type)) { xhr.setRequestHeader(X-CSRFToken, csrftoken); } } }); // 如果使用fetch function fetchWithCsrf(url, options {}) { const defaultOptions { headers: { X-CSRFToken: csrftoken, Content-Type: application/json, }, }; return fetch(url, { ...defaultOptions, ...options }); }4.4 实现登录注册视图与权限装饰器在accounts/views.py中实现登录、注册、登出视图。from django.shortcuts import render, redirect from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required, permission_required from django.views.decorators.http import require_http_methods from .forms import LoginForm, RegisterForm require_http_methods([GET, POST]) def login_view(request): if request.user.is_authenticated: return redirect(dashboard) if request.method POST: form LoginForm(request.POST) if form.is_valid(): username form.cleaned_data[username] password form.cleaned_data[password] user authenticate(request, usernameusername, passwordpassword) if user is not None: if user.is_active: login(request, user) next_url request.GET.get(next, dashboard) return redirect(next_url) else: form.add_error(None, 账号已被禁用) else: form.add_error(None, 用户名或密码错误) else: form LoginForm() return render(request, accounts/login.html, {form: form}) login_required def logout_view(request): logout(request) return redirect(accounts:login) login_required permission_required(accounts.view_customuser, raise_exceptionTrue) def user_list(request): users CustomUser.objects.all() return render(request, accounts/user_list.html, {users: users})4.5 配置URL与模板的完整流程在accounts/urls.py中配置路由。from django.urls import path from . import views app_name accounts urlpatterns [ path(login/, views.login_view, namelogin), path(logout/, views.logout_view, namelogout), path(register/, views.register_view, nameregister), path(users/, views.user_list, nameuser_list), ]在项目根urls.py中包含这个路由并配置静态文件和媒体文件的服务。模板方面登录页面需要包含{% csrf_token %}并且表单的action指向登录视图。注册页面类似但需要额外处理密码确认和字段验证。整个流程跑通后你可以测试几个场景未登录访问受保护页面是否跳转到登录页、登录后是否正常访问、CSRF token缺失时是否返回403、权限不足时是否返回403。这些场景覆盖了中间件、CSRF、auth三者的核心功能。5. 常见问题与排查技巧实录5.1 CSRF验证失败的六种典型原因CSRF 403错误是Django开发中最常见的问题之一。根据我的经验原因通常集中在以下几个方面。问题现象可能原因排查方法表单提交报403模板中缺少{% csrf_token %}检查模板文件AJAX请求报403请求头中没有X-CSRFToken检查前端代码登录后仍报403Cookie中的token与session不匹配清除Cookie重试跨域请求报403CSRF_TRUSTED_ORIGINS未配置检查settings配置子域名下报403CSRF_COOKIE_DOMAIN设置不当调整Cookie域移动端报403SameSite策略限制调整CSRF_COOKIE_SAMESITE排查CSRF问题时我通常先看浏览器开发者工具中的Network面板确认请求头中是否携带了X-CSRFToken以及Cookie中是否有csrftoken。如果两者都有但值不一致说明token没有正确同步。这时候可以尝试在视图上临时加上csrf_exempt装饰器来确认问题是否出在CSRF上但切记这只是排查手段不要留在生产代码里。5.2 auth认证中的session与权限陷阱auth系统最常见的问题是session丢失和权限判断错误。session丢失通常是因为SESSION_COOKIE_SECURE设置为True但网站跑在HTTP上或者SESSION_COOKIE_SAMESITE设置过严导致跨站请求不带Cookie。权限判断错误则往往是因为没有正确使用has_perm()方法或者权限名称拼写错误。Django的权限名称格式是app_label.action_modelname比如accounts.view_customuser。在模板中可以用{% if perms.accounts.view_customuser %}来判断。在视图中用request.user.has_perm(accounts.view_customuser)。如果权限判断始终返回False先确认用户是否已经登录再确认权限是否已经分配给用户或用户所在的组。提示超级用户is_superuserTrue自动拥有所有权限不需要单独分配。但is_staffTrue只影响admin后台的访问不影响普通视图的权限判断。5.3 中间件顺序错误的排查与修复中间件顺序错误导致的问题往往比较隐蔽。比如登录后仍然被重定向到登录页很可能是因为自定义的登录检查中间件放在了AuthenticationMiddleware之前导致request.user还是匿名用户。又比如CSRF验证始终失败可能是因为CsrfViewMiddleware被放在了自定义中间件之后而自定义中间件修改了请求体。排查这类问题的办法是打印中间件的执行顺序。可以在每个中间件的process_request和process_response中加日志观察请求和响应的经过顺序。正常情况下请求阶段的顺序和MIDDLEWARE列表顺序一致响应阶段则相反。我在实际项目中总结了一个中间件顺序的口诀安全第一、session第二、通用第三、CSRF第四、认证第五、消息第六、自定义最后。这个顺序适用于绝大多数场景如果自定义中间件需要修改request.user就放在认证之后如果需要拦截所有请求就放在最前面。5.4 自定义用户模型的迁移与兼容问题自定义用户模型最大的坑是迁移。如果项目已经运行过migrate再修改AUTH_USER_MODEL会直接报错。解决办法只有两个要么删除数据库和所有迁移文件重新开始要么创建一个新的用户模型并通过数据迁移把旧数据导过去。前者适合开发阶段后者适合生产环境。另一个常见问题是自定义用户模型后admin后台的注册需要调整。默认的UserAdmin是针对内置User模型的自定义模型需要继承UserAdmin并添加额外字段的配置。from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import CustomUser admin.register(CustomUser) class CustomUserAdmin(UserAdmin): list_display (username, email, phone, is_staff, is_active) fieldsets UserAdmin.fieldsets ( (扩展信息, {fields: (phone, nickname, avatar)}), ) add_fieldsets UserAdmin.add_fieldsets ( (扩展信息, {fields: (phone, nickname)}), )5.5 性能优化与安全加固的实操建议当用户量上来之后auth系统的性能会成为瓶颈。每次请求都要从数据库加载用户对象如果并发量高数据库压力会很大。优化方案是使用缓存。Django的session可以配置为缓存session用户对象也可以通过cached_property缓存到请求周期内。安全加固方面有几个配置项建议在生产环境中开启SESSION_COOKIE_SECURE True确保session只通过HTTPS传输CSRF_COOKIE_SECURE True同理SECURE_HSTS_SECONDS开启HSTSSECURE_CONTENT_TYPE_NOSNIFF True防止MIME类型嗅探。这些配置在开发环境可以关闭但上线前一定要检查。另外登录接口建议加上频率限制防止暴力破解。Django本身没有内置频率限制但可以用缓存实现一个简单的计数器或者引入第三方库。我通常会在登录视图里加一个基于IP和用户名的缓存计数超过阈值就锁定一段时间。6. 我在实际项目中的几点体会中间件、CSRF、auth这三块内容单独看都不复杂但组合在一起时问题的排查难度会成倍增加。我的经验是遇到403错误先看CSRF遇到重定向循环先看中间件顺序遇到权限判断异常先看用户是否登录。这三个排查方向能覆盖八成以上的问题。另外不要试图自己实现一套认证系统。Django内置的方案经过多年打磨安全性和稳定性都远超个人实现。把时间花在理解它的工作机制上比重新造轮子划算得多。如果确实需要扩展优先考虑继承和组合而不是替换。最后分享一个小技巧在开发阶段可以在settings.py中加一个DEBUG判断当DEBUGTrue时打印所有中间件的执行顺序和CSRF token的校验结果。这样排查问题时不用反复加日志效率会高很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑