ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Django Cookie与Session深度解析:登录态维持、安全配置与故障排查

Django Cookie与Session深度解析:登录态维持、安全配置与故障排查 1. 从一次登录态丢失说起Django Cookie/Session 到底在解决什么问题做过 Django 项目的人大概率都遇到过这种场景本地开发时登录一切正常部署到服务器后用户刚登录刷新一下页面就退出了或者同一个浏览器开了两个标签页一个登录了管理员另一个登录普通用户结果互相把对方的登录态顶掉了。这类问题十有八九都指向同一个核心机制——Cookie 与 Session。Cookie 和 Session 是 Web 开发里绕不开的基础设施。简单说HTTP 协议本身是“无状态”的服务器处理完一个请求就忘了你是谁下一次请求来了它完全不认识。Cookie 是浏览器端存的一小段文本Session 是服务端存的一份用户数据两者配合起来才能让服务器“记住”用户。Django 作为一款成熟的 Python Web 框架把 Cookie 和 Session 的读写封装得非常顺手但封装得好不代表没有坑很多细节如果不清楚排查问题时会非常痛苦。这篇内容适合正在用 Django 做项目、被登录态问题困扰过的开发者也适合想搞清楚 Cookie 与 Session 底层原理的初学者。我会从 Django 的实际代码出发把 Cookie 的读写、Session 的存储引擎选择、登录态维持、安全配置、常见故障排查这几块讲透中间穿插我自己踩过的坑和实测有效的解决方案。读完你应该能独立处理绝大多数和登录态相关的问题而不是遇到就上网搜半天。2. Django 中 Cookie 与 Session 的整体设计思路2.1 为什么 Django 默认用 Session 而不是把用户信息塞进 Cookie刚接触 Web 开发时我也有过疑问既然 Cookie 能存数据为什么不直接把用户名、角色这些信息加密后放进 Cookie每次请求带过来不就行了这样服务端连存储都省了。这个思路其实就是后来流行的 JWTJSON Web Token的雏形但 Django 默认没有走这条路原因很实在。Cookie 存在用户浏览器里哪怕加密了也意味着数据在客户端手里。一旦密钥泄露或者加密算法被攻破攻击者就能伪造身份。而且 Cookie 有大小限制一般单个 Cookie 不超过 4KB存不了太多东西。更关键的是服务端无法主动让一个已经发出去的 Cookie 失效——用户注销了但那个 Cookie 只要没过期照样能用。Session 就不一样了真正的用户数据存在服务端浏览器只拿到一个随机生成的 session key服务端想踢谁下线把对应的 Session 记录删掉就行控制权完全在自己手里。Django 的设计哲学是“安全默认”所以它选择把敏感数据放服务端客户端只留一个无意义的标识符。这个标识符就是 sessionid通常存在名为sessionid的 Cookie 里。理解了这一点后面很多配置和排查思路就顺了。2.2 Cookie 与 Session 在 Django 请求生命周期中的位置一个请求进入 Django 后中间件Middleware会按顺序处理。Session 相关的逻辑主要在SessionMiddleware里它会在请求进来时根据 Cookie 里的 sessionid 去存储后端加载对应的 Session 数据挂到request.session上响应返回时如果 Session 有改动再写回存储并设置 Cookie。Cookie 的处理则更底层一些request.COOKIES是一个字典直接读就行设置 Cookie 用response.set_cookie()。Session 的读写则通过request.session这个类字典对象用起来和普通 dict 几乎一样但背后涉及存储引擎的读写和序列化。这里有个容易忽略的点Session 的保存不是实时的。你在视图里改了request.session[key] value数据不会立刻写进数据库或缓存而是等到响应阶段由中间件统一保存。这意味着如果你在视图里手动构造了一个新的 HttpResponse 并直接返回绕过了中间件的处理Session 可能就不会被保存。我早期写 API 接口时就因为这个丢过登录态排查了很久才发现是返回方式的问题。2.3 存储引擎的选择逻辑与取舍Django 支持多种 Session 存储后端配置项是SESSION_ENGINE。常见的有数据库、缓存、缓存加数据库、文件、签名 Cookie 几种。选哪个不是拍脑袋决定的得看项目规模和部署方式。数据库存储是默认选项好处是持久化、可靠重启服务不丢数据坏处是每次请求都要查一次库并发高的时候数据库压力明显。缓存存储比如 Redis读写快适合高并发场景但缓存一重启数据就没了用户会集体掉线。缓存加数据库的组合是折中方案平时走缓存缓存里没有再去数据库捞兼顾速度和持久性。文件存储适合单机小项目多机部署就废了。签名 Cookie 存储把整个 Session 数据加密后放回 Cookie服务端不存东西适合无状态场景但同样有 Cookie 大小限制和数据可控性问题。我的经验是中小项目直接用数据库存储省心日活上万的项目上 Redis 缓存存储同时配一个持久化策略如果用了 Redis 又担心重启丢数据就上cached_db。这个选择没有绝对的对错只有适不适合当前的部署架构。3. Cookie 的读写操作与关键参数详解3.1 读取 Cookie 的正确姿势与编码陷阱在 Django 视图里读 Cookie 非常直接request.COOKIES就是一个普通字典。比如用户上次选择的语言存在lang这个 Cookie 里你可以这样拿def index(request): lang request.COOKIES.get(lang, zh-cn) return render(request, index.html, {lang: lang})看起来简单但中文 Cookie 是个大坑。Cookie 的值按规范只能是 ASCII 字符如果你直接response.set_cookie(name, 张三)Django 会尝试编码但不同浏览器和不同 Django 版本的处理方式不完全一致有时候读出来是乱码。稳妥的做法是在写入前自己用urllib.parse.quote编码读取时用unquote解码from urllib.parse import quote, unquote # 写入 response.set_cookie(nickname, quote(张三)) # 读取 nickname unquote(request.COOKIES.get(nickname, ))我见过有项目直接把中文塞进 Cookie在 Chrome 上测试没问题换到某些手机浏览器就乱码了排查半天。所以涉及非 ASCII 字符手动编码这一步别省。3.2 set_cookie 的核心参数逐个拆解response.set_cookie()的参数不少每个都有实际意义用错了就是安全隐患或者功能异常。参数作用常用取值与建议keyCookie 名称避免用下划线开头部分服务器会过滤valueCookie 值非 ASCII 需自行编码max_age有效期秒不设则关闭浏览器即失效expires过期时间点与 max_age 二选一max_age 优先级更高path生效路径默认/全站生效domain生效域名跨子域共享时设置如.example.comsecure仅 HTTPS 传输生产环境务必设为 Truehttponly禁止 JS 读取防 XSS 窃取登录态 Cookie 必开samesite跨站请求策略推荐Lax严格场景用Strictmax_age和expires的区别值得说一下。max_age是相对时间从设置那一刻算起多少秒后过期expires是绝对时间点。现代浏览器都支持max_age而且它不受客户端时钟影响所以优先用它。expires主要是兼容非常老的浏览器现在基本可以不用。httponly这个参数我要重点强调。它的作用是让 Cookie 无法通过 JavaScript 的document.cookie读取。如果你的登录凭证存在 Cookie 里而没开这个选项一旦页面存在 XSS 漏洞攻击者一行 JS 就能把你的登录态偷走。Django 的 Session Cookie 默认就开了 httponly但你自己手动设的业务 Cookie 一定要记得加。3.3 删除 Cookie 与路径匹配的坑删除 Cookie 不是简单调个delete_cookie就完事有个细节很多人栽过跟头删除时必须和设置时的 path、domain 完全一致否则删不掉。# 设置时 response.set_cookie(token, abc, path/user/) # 删除时也必须指定同样的 path response.delete_cookie(token, path/user/)如果你设置时用了path/user/删除时用默认的/浏览器会认为这是两个不同的 Cookie结果就是旧的删不掉新的又设了一个越积越多。这个坑我在做一个多模块项目时遇到过用户注销后 Cookie 还在导致重新登录时读到了旧值。排查方法很简单打开浏览器开发者工具的 Application 面板看 Cookie 列表里的 Path 列一目了然。4. Session 的配置、读写与存储引擎实战4.1 开启 Session 与基础配置项Django 新建项目默认就启用了 Session前提是INSTALLED_APPS里有django.contrib.sessionsMIDDLEWARE里有django.contrib.sessions.middleware.SessionMiddleware。这两个都在Session 就能用。核心配置项集中在 settings.py 里# 存储引擎默认数据库 SESSION_ENGINE django.contrib.sessions.backends.db # Cookie 名称默认 sessionid SESSION_COOKIE_NAME sessionid # Cookie 有效期默认两周1209600 秒 SESSION_COOKIE_AGE 1209600 # 浏览器关闭即失效默认 False SESSION_EXPIRE_AT_BROWSER_CLOSE False # 仅 HTTPS 传输 SESSION_COOKIE_SECURE False # 禁止 JS 读取 SESSION_COOKIE_HTTPONLY True # 跨站策略 SESSION_COOKIE_SAMESITE LaxSESSION_COOKIE_AGE和SESSION_EXPIRE_AT_BROWSER_CLOSE这两个配合使用能实现不同的登录体验。如果SESSION_EXPIRE_AT_BROWSER_CLOSE设为 True那SESSION_COOKIE_AGE就失效了用户关掉浏览器登录态就没了。很多后台管理系统希望管理员关掉浏览器就退出就设这个为 True。面向普通用户的产品一般设 False让登录态保持两周体验更好。4.2 request.session 的增删改查实操request.session用起来像字典但有几个专属方法需要记住def login_view(request): # 写入 request.session[user_id] user.id request.session[role] admin # 读取带默认值 user_id request.session.get(user_id) # 判断是否存在 if user_id in request.session: pass # 删除单个键 del request.session[role] # 清空当前会话的所有数据但保留 session key request.session.flush() # 只清数据保留 key 和过期时间 request.session.clear() # 设置过期时间秒 request.session.set_expiry(3600) # 让会话在浏览器关闭时过期 request.session.set_expiry(0)flush()和clear()的区别要分清。flush()会删除整个 Session 记录并生成新的 session key适合注销登录时用彻底断干净。clear()只清空数据key 还在适合清空购物车这类场景。注销时用错方法可能导致旧 session key 还能被利用存在安全隐患。set_expiry()的取值也有讲究传整数是秒数传 0 是浏览器关闭即过期传 None 是用全局的SESSION_COOKIE_AGE传 datetime 对象是具体时间点。我一般注销时用flush()临时提升权限时用set_expiry(600)设个十分钟的短会话。4.3 数据库、缓存、cached_db 三种引擎的实测对比光说理论不够我拿一个实际项目做过压测对比。场景是模拟 500 并发用户持续请求一个需要读 Session 的接口服务器是 4 核 8G数据库和 Redis 都在同机。存储引擎平均响应时间QPS重启后登录态适用场景db45ms约 1100保留中小项目、单机cacheRedis12ms约 4000丢失高并发、可接受掉线cached_db15ms约 3500保留高并发且要求持久数据很直观数据库存储的响应时间明显更长因为每次请求都要走一次数据库查询。缓存存储快了近四倍但代价是 Redis 重启后所有用户掉线。cached_db兼顾了两者平时读缓存缓存没有才查库写入时两边都写。配置cached_db需要同时指定缓存和数据库SESSION_ENGINE django.contrib.sessions.backends.cached_db SESSION_CACHE_ALIAS default # 对应 CACHES 里的配置这里有个细节cached_db写入时是先写缓存再写数据库如果数据库写入失败缓存里已经有了会出现数据不一致。不过 Session 数据本身不是强一致性要求偶尔不一致影响不大。真正要紧的是别把 Session 存在一个随时会丢的缓存里还不做持久化那样用户会莫名其妙被登出。5. 登录态维持的完整实现与安全加固5.1 从零实现一个基于 Session 的登录流程Django 自带了django.contrib.auth登录登出都有现成函数但理解底层实现很有必要。下面是一个不依赖 auth 框架的最小登录实现帮你彻底搞懂 Session 登录是怎么回事。from django.http import JsonResponse from django.views.decorators.http import require_POST from django.contrib.auth.hashers import check_password from .models import User require_POST def login(request): username request.POST.get(username) password request.POST.get(password) try: user User.objects.get(usernameusername) except User.DoesNotExist: return JsonResponse({code: 1, msg: 用户名或密码错误}) if not check_password(password, user.password): return JsonResponse({code: 1, msg: 用户名或密码错误}) # 登录成功写入 Session request.session[user_id] user.id request.session[username] user.username # 设置会话有效期为一小时 request.session.set_expiry(3600) return JsonResponse({code: 0, msg: 登录成功})注意这里用户名不存在和密码错误返回了同样的提示这是有意为之。如果分别提示“用户不存在”和“密码错误”攻击者就能通过提示枚举出哪些用户名是有效的为后续爆破提供便利。这个细节很多教程不讲但实际项目里很重要。登录成功后后续请求通过一个装饰器来校验登录态from functools import wraps from django.http import JsonResponse def login_required_session(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): if user_id not in request.session: return JsonResponse({code: 401, msg: 请先登录}) return view_func(request, *args, **kwargs) return wrapper5.2 Session 固定攻击与登录后重新生成 KeySession 固定攻击Session Fixation是一个容易被忽视的安全问题。攻击思路是攻击者先访问网站拿到一个 session key然后想办法让受害者的浏览器也用这个 key比如通过链接参数诱导等受害者用这个 key 登录后攻击者手里就有了一个已登录的 session key直接冒充受害者。防御方法很简单用户登录成功后立刻重新生成 session key。Django 提供了cycle_key()方法def login(request): # ... 验证用户名密码 ... if 验证通过: request.session.cycle_key() # 保留数据换新 key request.session[user_id] user.idcycle_key()会保留当前 Session 里的数据但生成一个新的 session key旧的 key 立即失效。这样即使攻击者之前拿到了 key登录后也作废了。Django 自带的login()函数内部已经做了这件事所以用 auth 框架的话不用操心但自己实现登录时一定要加上。5.3 生产环境的 Cookie 安全配置清单上线前下面这几项配置必须检查缺一个都可能是隐患# 生产环境配置 SESSION_COOKIE_SECURE True # 仅 HTTPS 传输 SESSION_COOKIE_HTTPONLY True # 禁止 JS 读取 SESSION_COOKIE_SAMESITE Lax # 防 CSRF CSRF_COOKIE_SECURE True CSRF_COOKIE_HTTPONLY TrueSESSION_COOKIE_SECURE True意味着 Cookie 只在 HTTPS 连接中发送。如果你的站点还没上 HTTPS开了这个会导致登录态完全失效所以要在确认 HTTPS 配置无误后再开。我见过有人在测试环境开了这个结果怎么都登录不上排查半天才发现是 HTTP 访问导致的。SameSite设为Lax是现在的推荐值它能阻止大部分跨站请求携带 Cookie有效缓解 CSRF 攻击同时不影响正常的链接跳转。如果设成Strict用户从外部链接点进来时不会带 Cookie会显示未登录状态体验不好。None则要求必须同时设Secure否则浏览器会拒绝。6. 常见故障排查与避坑经验实录6.1 登录态丢失问题速查表登录态丢失是最高频的问题原因五花八门。我整理了一张速查表按可能性从高到低排列现象可能原因排查方法刷新即退出Session 未保存检查是否手动返回 Response 绕过了中间件部署后登录失效多进程密钥不一致确认 SECRET_KEY 固定未随机生成部分用户掉线缓存存储重启检查 Redis 是否重启考虑换 cached_db跨子域不共享Cookie domain 未设设置 SESSION_COOKIE_DOMAINHTTPS 下登录失败Secure 配置与协议不符确认站点协议与 SESSION_COOKIE_SECURE 匹配手机浏览器异常SameSite 策略过严调整为 Lax 或检查跨站场景其中“多进程密钥不一致”这个坑值得展开说。Django 的 Session 数据默认是用SECRET_KEY签名或加密的如果部署时用了多个 worker 进程而每个进程启动时随机生成了不同的 SECRET_KEY那 A 进程写的 SessionB 进程读的时候验签失败就会当成无效会话。解决办法是 SECRET_KEY 必须写在配置文件或环境变量里所有进程读同一个值。用 Gunicorn 或 uWSGI 多 worker 部署时尤其要注意。6.2 Session 表膨胀与清理策略用数据库存储 Session 时django_session表会随着用户访问不断增长。虽然过期的 Session 在读取时会被判定失效但记录不会自动删除时间长了表会很大影响查询性能。Django 提供了清理命令python manage.py clearsessions这个命令会删除所有已过期的 Session 记录。建议配一个定时任务每天凌晨跑一次。如果是用 Celery 的项目也可以写个定时任务调用。我见过一个项目上线半年没清理session 表涨到几百万行登录接口越来越慢清理后立刻恢复正常。另外如果用的是缓存存储过期由缓存自己处理不用操心。但要注意缓存的淘汰策略如果内存不够被 LRU 淘汰了用户也会掉线所以缓存容量要留够。6.3 多标签页与多设备登录的处理同一个账号在多处登录Session 怎么处理默认情况下每次登录都会生成新的 session key旧的自然失效也就是后登录的顶掉先登录的。如果产品要求允许多设备同时在线就不能只存一个 session key得在服务端维护一个用户到多个 session 的映射。一种常见做法是单独建一张表记录用户的活跃会话class UserSession(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) session_key models.CharField(max_length40) device models.CharField(max_length100) login_time models.DateTimeField(auto_now_addTrue)登录时插入一条记录校验时不仅看 Session 是否存在还要看这个 session key 是否在用户的活跃列表里。踢下线就是把对应记录删掉同时删除 Session 数据。这样就能实现查看登录设备、远程踢下线这些功能。实现起来不复杂但要想清楚产品到底需不需要别为了功能而功能。6.4 调试 Session 的实用技巧排查 Session 问题时光看代码有时候看不出所以然得借助工具。Django shell 是个好帮手python manage.py shellfrom django.contrib.sessions.models import Session from django.utils import timezone # 查看所有未过期的 Session active Session.objects.filter(expire_date__gtetimezone.now()) for s in active: data s.get_decoded() print(s.session_key, data)get_decoded()能把 Session 里存的原始数据解出来看看里面到底有没有你写入的键值。如果发现数据没写进去就往回查写入逻辑如果写进去了但读不到就查 session key 在请求间是否一致。浏览器开发者工具的 Application 面板也是必备。看 Cookies 里有没有 sessionid值是什么Path 和 Domain 对不对过期时间是什么。很多时候问题一眼就能看出来比如发现 sessionid 的 Domain 是www.example.com而当前访问的是example.com那自然读不到。7. 一些容易被忽略的边界情况7.1 并发请求下的 Session 竞态同一个用户同时发多个请求如果这些请求都修改 Session可能出现竞态。比如两个请求同时读到count1各自加一写回结果都是 2丢了一次更新。Django 的 Session 默认没有加锁机制这种竞态在高并发下是真实存在的。解决办法有几个一是把需要原子操作的计数放到数据库或 Redis 里用原子命令处理不要放 Session二是用select_for_update对 Session 记录加行锁但这样会降低并发三是接受最终一致性Session 数据本来就不适合做精确计数。我的建议是 Session 只存身份标识和少量状态别把它当数据库用。7.2 Session 数据序列化格式的选择Django 默认用 JSON 序列化 Session 数据配置项是SESSION_SERIALIZER。早期版本默认用 pickle但 pickle 有反序列化执行任意代码的风险所以新版本都改成了 JSON。如果你从老项目升级上来记得检查这个配置SESSION_SERIALIZER django.contrib.sessions.serializers.JSONSerializerJSON 序列化有个限制只能存 JSON 支持的类型datetime、Decimal 这些存进去会报错。如果确实需要存复杂对象要么转成字符串要么自定义序列化器。我一般建议 Session 里只存基本类型复杂数据存数据库用 id 关联。7.3 前后端分离场景下的 Session 处理现在很多项目是前后端分离的前端可能是 Vue 或 React后端只提供 API。这种场景下 Cookie 和 Session 还能用但要注意跨域配置。前端请求要带上withCredentials: true后端要设置 CORS 允许携带凭证CORS_ALLOW_CREDENTIALS True CORS_ALLOWED_ORIGINS [https://frontend.example.com]同时SESSION_COOKIE_SAMESITE可能要设为None并配合Secure因为前后端不同域属于跨站。但None会带来 CSRF 风险所以必须配合 CSRF Token 使用。如果觉得这套配置太麻烦前后端分离项目也可以考虑用 Token 方案把登录凭证放请求头里避开 Cookie 的跨域限制。两种方案各有取舍Session 方案服务端可控性强Token 方案跨域友好看项目实际情况选。8. 我在实际项目中的几点体会做了几个 Django 项目下来关于 Cookie 和 Session我最深的体会是默认配置能覆盖大部分场景但一旦出问题往往出在那些“以为不用管”的细节上。比如 SECRET_KEY 的固定、Session 表的清理、Cookie 的 path 匹配这些平时不显眼出问题时又很难第一时间想到。另一个体会是安全配置不要等到上线才加。开发阶段就把HTTPONLY、SAMESITE这些设好用 HTTPS 开发这样上线时不会因为环境差异导致登录态异常。我吃过这个亏本地 HTTP 开发一切正常上线 HTTPS 后因为 Secure 配置没同步登录直接失效紧急回滚才解决。最后分享一个小技巧如果你不确定 Session 里到底存了什么除了用 shell 查还可以写一个临时的调试视图把request.session的内容直接返回出来。当然这个视图只能在内网或者开发环境用上线前务必删掉。排查问题时能看到真实数据比猜来猜去高效得多。
返回列表