ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Django 用户认证与安全机制:CSRF、Auth 模块及自定义用户模型详解

2026/8/19 12:59:02 拓冰建站 浏览量
Django 用户认证与安全机制:CSRF、Auth 模块及自定义用户模型详解 一、CSRF跨站请求伪造CSRFCross-Site Request Forgery是指攻击者利用用户已经登录的浏览器诱导浏览器向目标网站发送一个用户没有主动发起的请求。浏览器会自动携带目标网站的 Cookie因此如果服务端只依赖 Cookie 判断身份就可能误把伪造请求当成正常操作。例如用户登录了支付网站后又打开一个恶意页面恶意页面可以隐藏一个指向支付接口的表单。用户点击按钮后浏览器可能自动带上支付网站的登录 Cookie导致服务端执行非预期操作。CSRF 的重点是“请求来自哪里”和“是否由用户在本站页面发起”而不是窃取用户密码。1. Django 的防护方式Django 默认通过CsrfViewMiddleware校验 CSRF Token。服务端页面生成一个随机 Token提交表单时必须把 Token 一并发送攻击者通常无法读取本站页面中的 Token因此伪造请求会被拒绝。# settings.py默认项目通常已经包含这一项MIDDLEWARE[# ...django.middleware.csrf.CsrfViewMiddleware,]HTML 表单中使用{% csrf_token %}输出隐藏字段。必须使用 POST、PUT、PATCH、DELETE 等修改数据的请求时才需要重点关注 CSRF 校验GET 请求应保持幂等不要用来执行删除或转账等操作。formaction/pay/methodpost{% csrf_token %}label收款账号inputtypetextnameto_account/labellabel金额inputtypenumbernamepay_moneymin0.01step0.01/labelbuttontypesubmit确认付款/button/form缺少 Token 或 Token 不正确时Django 通常返回 403 Forbidden。不要为了消除错误而直接注释掉 CSRF 中间件这会让所有相关视图失去默认保护。2. Ajax 请求携带 CSRF TokenAjax 不会自动把表单中的隐藏字段序列化因此需要主动读取 Token 并随请求发送。最简单的方式是从模板中的隐藏输入框读取constcsrfTokendocument.querySelector([namecsrfmiddlewaretoken]).value;fetch(/pay/,{method:POST,headers:{Content-Type:application/json,X-CSRFToken:csrfToken,},body:JSON.stringify({to_account:9696969696969696,pay_money:100.00,}),});如果使用 jQuery可以在全局 Ajax 配置中从csrftokenCookie 读取 Token并写入X-CSRFToken请求头。只应对非安全方法添加请求头GET、HEAD、OPTIONS、TRACE 通常不需要 Token。functiongetCookie(name){constitemdocument.cookie.split(;).map((value)value.trim()).find((value)value.startsWith(${name}));returnitem?decodeURIComponent(item.slice(name.length1)):null;}constcsrfTokengetCookie(csrftoken);$.ajaxSetup({beforeSend(xhr,settings){constsafeMethod/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type);if(!safeMethod!settings.crossDomain){xhr.setRequestHeader(X-CSRFToken,csrfToken);}},});跨域前后端分离项目还需要正确配置CSRF_TRUSTED_ORIGINS、CORS 和 Cookie 的SameSite策略。CSRF Token 不是登录凭证不能替代身份认证。二、Django Auth 模块Django 自带django.contrib.auth提供用户模型、密码哈希、登录状态、权限和分组等能力。执行迁移后默认会创建auth_user、auth_permission、auth_group及其关联表Session 则由django_session表保存使用数据库 Session 时。Auth 的权限模型可以概括为 RBAC基于角色的访问控制用户可以加入一个或多个分组分组拥有若干权限用户也可以直接拥有额外权限。权限一般对应模型的增删改查操作。对象作用User保存用户名、密码哈希、激活状态等用户信息Group把多个用户组织成角色例如编辑、管理员Permission描述某个模型上的 add/change/delete/view 权限User-Group / User-Permission保存用户与分组、权限之间的关联三、用户注册、登录与退出1. 创建用户不要使用User.objects.create(password...)保存用户密码因为这会把明文密码直接写入数据库Django 的认证系统也无法按正常方式校验它。使用create_user()它会自动调用密码哈希逻辑。fromdjango.contrib.auth.modelsimportUserfromdjango.httpimportHttpResponsefromdjango.viewsimportViewclassRegisterView(View):defpost(self,request,*args,**kwargs):usernamerequest.POST.get(username,).strip()passwordrequest.POST.get(password,)ifnotusernameornotpassword:returnHttpResponse(用户名和密码不能为空,status400)ifUser.objects.filter(usernameusername).exists():returnHttpResponse(用户名已存在,status400)User.objects.create_user(usernameusername,passwordpassword)returnHttpResponse(注册成功)超级管理员通常通过命令创建可以登录 Django Adminpython manage.py createsuperuser用户表中的密码类似pbkdf2_sha256$...这是带盐的密码哈希不是可逆加密。is_activeFalse可用于停用账号last_login的显示时间受项目时区配置影响。2. 登录并保存状态authenticate()会调用 Django 配置的认证后端比对密码成功返回用户对象失败返回None。login()会把用户信息写入 Session使后续请求能够通过request.user找到当前用户。fromdjango.contribimportauthfromdjango.httpimportHttpResponsefromdjango.shortcutsimportredirectfromdjango.urlsimportreversefromdjango.viewsimportViewclassLoginView(View):defpost(self,request,*args,**kwargs):usernamerequest.POST.get(username,).strip()passwordrequest.POST.get(password,)userauth.authenticate(request,usernameusername,passwordpassword,)ifuserisNone:returnHttpResponse(用户名或密码错误,status401)auth.login(request,user)# login_required 会把原访问地址放在 next 参数中。next_urlrequest.GET.get(next)ifnext_urlandnext_url.startswith(/):returnredirect(next_url)returnredirect(reverse(home))重定向到next前应限制其为本站地址避免产生开放重定向漏洞。生产项目还应使用表单校验、登录失败限流等措施。3. 判断登录状态和退出AuthenticationMiddleware会把当前用户放在request.user中。未登录时它是AnonymousUser不能通过字符串比较判断登录状态应使用is_authenticated。fromdjango.contribimportauthfromdjango.httpimportHttpResponsefromdjango.shortcutsimportredirectfromdjango.urlsimportreversedefhome(request):ifnotrequest.user.is_authenticated:returnHttpResponse(请先登录,status401)returnHttpResponse(f欢迎{request.user.username})deflogout_view(request):auth.logout(request)returnredirect(reverse(login))四、登录保护装饰器自己编写装饰器可以理解登录校验流程但实际项目优先使用 Django 已提供的login_required它能统一处理跳转地址和next参数。fromdjango.contrib.auth.decoratorsimportlogin_requiredfromdjango.httpimportHttpResponselogin_required(login_url/login/)defprivate_page(request):returnHttpResponse(只有登录用户才能看到)也可以在settings.py中统一指定登录地址这样装饰器不必重复传参LOGIN_URL/login/LOGIN_REDIRECT_URL/LOGOUT_REDIRECT_URL/login/如果需要自定义装饰器记得使用functools.wraps保留原函数的元数据并保持返回值为 Django 响应对象fromfunctoolsimportwrapsfromdjango.shortcutsimportredirectfromdjango.urlsimportreversedeflogin_auth(view_func):wraps(view_func)defwrapper(request,*args,**kwargs):ifrequest.user.is_authenticated:returnview_func(request,*args,**kwargs)login_urlreverse(login)returnredirect(f{login_url}?next{request.path})returnwrapper五、修改密码密码已经以哈希形式存储不能直接比较数据库字段也不能直接给password属性赋明文。使用check_password()校验旧密码使用set_password()生成新哈希保存后建议让用户重新登录。fromdjango.contrib.auth.decoratorsimportlogin_requiredfromdjango.contribimportauthfromdjango.httpimportHttpResponsefromdjango.shortcutsimportredirectlogin_requireddefchange_password(request):ifrequest.method!POST:returnHttpResponse(请使用 POST 请求,status405)old_passwordrequest.POST.get(old_password,)new_passwordrequest.POST.get(new_password,)ifnotrequest.user.check_password(old_password):returnHttpResponse(旧密码错误,status400)iflen(new_password)8:returnHttpResponse(新密码至少需要 8 位,status400)request.user.set_password(new_password)request.user.save(update_fields[password])auth.logout(request)# 密码变化后使当前登录状态失效returnredirect(login)六、扩展用户模型如果项目需要手机号、性别等额外字段推荐在项目初期继承AbstractUser创建自定义用户模型。必须在第一次迁移前设置AUTH_USER_MODEL项目已经大量迁移后再更换用户模型迁移成本会明显增加。# users/models.pyfromdjango.contrib.auth.modelsimportAbstractUserfromdjango.dbimportmodelsclassUser(AbstractUser):phonemodels.CharField(max_length11,blankTrue,verbose_name手机号)gendermodels.PositiveSmallIntegerField(nullTrue,blankTrue,verbose_name性别)# settings.pyAUTH_USER_MODELusers.User此后不要再直接导入django.contrib.auth.models.User而应使用get_user_model()或settings.AUTH_USER_MODEL以便代码兼容自定义用户模型fromdjango.contrib.authimportget_user_model Userget_user_model()active_usersUser.objects.filter(is_activeTrue)七、Django 配置对象在业务代码中应从django.conf导入settings不要直接导入项目目录下的settings.py。Django 的配置对象会先加载全局默认配置再加载DJANGO_SETTINGS_MODULE指向的项目配置因此可以正确处理环境变量、默认值和不同部署环境。fromdjango.confimportsettingsfromdjango.httpimportHttpResponsedefsite_info(request):# 读取项目 settings.py 中的自定义配置。site_namegetattr(settings,SITE_NAME,Django 网站)returnHttpResponse(site_name)项目配置中的常见选项包括DEBUGTrueALLOWED_HOSTS[127.0.0.1,localhost]LANGUAGE_CODEzh-hansTIME_ZONEAsia/ShanghaiUSE_TZTrueDEBUGFalse时必须正确设置ALLOWED_HOSTS不能在生产环境长期使用*。USE_TZTrue时 Django 会以带时区的 UTC 时间保存和处理时间再根据TIME_ZONE展示给用户。部署时通常通过环境变量分别管理数据库、密钥、调试模式等敏感配置。八、总结CSRF Token 用于确认修改请求来自本站页面Ajax 请求需要通过表单字段或X-CSRFToken请求头主动携带。Auth 模块负责安全的密码哈希、认证、Session 登录状态和权限管理创建用户必须使用create_user()。使用authenticate()、login()、logout()管理登录流程用request.user.is_authenticated判断状态。使用login_required保护页面使用check_password()和set_password()修改密码。自定义用户模型应尽早建立并通过get_user_model()获取当前用户模型。