
Django 的 Cookie 和 Session 机制是每个后端开发者在做用户登录、购物车、权限校验时绕不开的基础设施。但我在带团队和做代码审查的过程中发现很多人对它的理解停留在“会用 request.session 存个 user_id”这个层面一旦遇到跨域、多端登录、Session 丢失、Cookie 被篡改这类问题排查起来就完全没有方向。这篇内容我打算把 Django 里 Cookie 和 Session 的完整链路拆开讲清楚从浏览器和服务器的交互原理到 Django 内部的存储引擎选型再到实际项目里登录态设计的取舍最后落到几个我真实踩过的坑和排查思路。适合已经写过 Django 项目、但对登录态机制只停留在“能跑就行”阶段的开发者也适合正在准备面试、想把这块知识体系补完整的朋友。1. 从一次登录态丢失的排查说起1.1 问题的表象和第一反应前阵子有个同事找我说他负责的一个 Django 后台管理系统用户登录之后操作几分钟再点任何接口就跳回登录页但刷新一下又好了过一会儿又掉。他第一反应是 Session 过期时间设太短了去 settings 里把 SESSION_COOKIE_AGE 从默认的 1209600 秒两周改成了更长的值结果问题依旧。第二个反应是怀疑前端请求没带 Cookie抓包一看Cookie 确实带上了sessionid 也在但服务器返回的是 302 重定向到登录页。这个现象其实很典型Cookie 在Session 却“找不到”了。要理解为什么必须先把 Cookie 和 Session 各自负责什么、它们怎么配合这件事讲透。很多人把这两个概念混在一起觉得“Session 就是存在服务器上的 Cookie”这个说法不准确也正因为这个模糊认知导致排查时抓不住重点。1.2 Cookie 和 Session 到底谁存了什么我用一个生活化的类比来解释。Cookie 像是你去游乐园时拿到的一张手环手环上只印了一个编号比如“A12345”。Session 则是游乐园后台系统里的一张记录表编号 A12345 对应着“张三今天入园已玩过三个项目”。手环本身不包含你的身份信息它只是一个凭证真正的信息在后台的记录表里。对应到 DjangoCookie是浏览器端存储的一小段键值对Django 默认用来存 sessionid 这个键值是服务器生成的一串随机字符串。它存在用户的浏览器里每次请求会自动带上。Session是服务器端的存储Django 默认存在数据库表django_session里包含 session_key、session_data经过编码的字典、expire_date 三个核心字段。所以“Session 丢失”通常不是 Cookie 没了而是服务器拿着 Cookie 里的 session_key 去存储里查查不到对应记录或者记录已过期。我那位同事的问题最后定位到是数据库里django_session表的记录被一个定时清理任务误删了而 Cookie 还在浏览器里于是每次请求都带着一个“孤儿 sessionid”服务器查不到就跳登录。这个案例说明排查登录态问题第一步永远是分清“是 Cookie 的问题还是 Session 的问题”。1.3 为什么默认配置下问题不容易暴露Django 默认的 Session 引擎是数据库存储django.contrib.sessions.backends.db配合默认的SESSION_ENGINE和SESSION_COOKIE_AGE在单机、低并发、没有外部清理任务的场景下基本不会出问题。这也是为什么很多教程和 Demo 里大家感觉 Session 很“稳”。但一旦上了生产环境多进程、多机部署、Redis 缓存、定时任务这些因素叠加默认配置的隐患就会集中爆发。下面几节我会把 Django 提供的几种 Session 存储引擎逐一拆开讲清楚每种适合什么场景、有什么坑。2. Django 四种 Session 存储引擎的取舍逻辑2.1 数据库存储默认选项的适用边界数据库存储是 Django 开箱即用的方案SESSION_ENGINE django.contrib.sessions.backends.db。它的工作流程是用户登录后Django 生成一个随机的 session_key把用户数据序列化后写入django_session表同时通过响应头Set-Cookie把 sessionid 发给浏览器。这个方案最大的优点是持久化和无需额外依赖。服务器重启、进程重启Session 都还在。适合中小型项目、后台管理系统、对 Session 读取频率不高的场景。但它的短板也很明显。每次请求都要查一次数据库高并发下django_session表会成为热点。我实测过一个日活几万的系统登录态校验接口 QPS 到几百的时候这张表的查询就开始拖慢整体响应。另外django_session表如果没有定期清理会无限膨胀。Django 提供了clearsessions管理命令但需要你手动配置定时任务去跑很多人根本不知道这个命令的存在。提示使用数据库存储时务必给django_session表的expire_date字段加索引Django 迁移时默认已加并配置python manage.py clearsessions的定时执行否则过期数据会一直堆积。2.2 缓存存储性能优先但要防丢缓存存储的配置是SESSION_ENGINE django.contrib.sessions.backends.cacheSession 数据存在你配置的缓存后端里最常见的是 Redis 或 Memcached。读写都在内存速度比数据库快一个数量级适合高并发、对登录态读取频繁的场景。这里有个关键选择cache和cached_db的区别。cache只写缓存缓存一挂所有 Session 全丢用户全部掉线。cached_db则是同时写缓存和数据库读的时候优先读缓存缓存没有就回源数据库。我个人的经验是生产环境如果要用缓存存 Session优先选cached_db用一点写入性能换数据可靠性这笔账很划算。纯cache模式只适合那种“掉线了重新登录也无所谓”的场景比如一些临时活动页。配置 Redis 作为缓存后端时SESSION_CACHE_ALIAS要和你CACHES里的别名对上这个细节经常被忽略导致 Session 实际写到了默认的本地内存缓存里多进程部署时进程之间不共享表现就是“一会儿登录一会儿掉线”。2.3 缓存加数据库可靠性与性能的平衡点cached_db是我在多数生产项目里的默认选择。它的读取逻辑是先查缓存命中就直接返回没命中就查数据库查到后回写缓存。写入逻辑是同时写缓存和数据库。这个方案的好处是即使 Redis 重启或者被清空Session 也不会丢用户无感知。代价是每次写 Session 都要落一次库但这个写入频率远低于读取频率所以整体性能依然比纯数据库方案好很多。有一个坑要注意cached_db模式下缓存和数据库的数据一致性依赖 Django 自身的逻辑如果你在代码里绕过 Django 直接操作了数据库里的 session 记录缓存不会同步更新就会出现“数据库里删了但缓存里还在”的情况。所以不要手动去改django_session表。2.4 签名 Cookie 存储无状态但容量受限SESSION_ENGINE django.contrib.sessions.backends.signed_cookies这个方案比较特殊它把整个 Session 数据经过签名后直接存在 Cookie 里服务器不存任何东西。优点是彻底无状态不依赖任何存储适合分布式部署且不想引入 Redis 的场景。但它的限制很硬Cookie 有大小限制通常 4KB 左右Session 数据稍微多一点就存不下。而且虽然数据经过签名防篡改但内容是 Base64 编码的用户可以看到 Session 里的所有内容所以绝对不能往里存敏感信息。另外SESSION_COOKIE_HTTPONLY对它无效因为数据本身就在 Cookie 里。我一般只在一些极简的、无状态的微服务里用它主站不会选。下面这张表把四种引擎的核心差异列清楚方便你对照选型引擎存储位置性能可靠性适用场景db数据库表一般高中小项目、后台系统cache缓存高低临时活动、可容忍掉线cached_db缓存数据库较高高生产环境首选signed_cookies浏览器 Cookie高中无状态微服务、数据量小3. Cookie 安全属性的实战配置3.1 HttpOnly 与 Secure 到底防住了什么Cookie 的安全属性里HttpOnly和Secure是最基础的两个。HttpOnly的作用是禁止 JavaScript 通过document.cookie读取这个 Cookie。为什么重要因为一旦网站存在 XSS 漏洞攻击者注入的脚本如果能读到 sessionid就能直接冒充用户。加上HttpOnly后脚本读不到攻击成本就高了很多。Django 默认SESSION_COOKIE_HTTPONLY True这个默认值是对的不要关。Secure属性则是要求 Cookie 只能通过 HTTPS 传输防止在 HTTP 明文传输中被中间人截获。生产环境必须开SESSION_COOKIE_SECURE True但要注意开了之后本地开发用 HTTP 访问会带不上 Cookie所以本地开发环境要单独配置。我见过有团队为了图省事生产环境也没开Secure理由是“我们内网访问”。这个理由站不住脚内网一样有被抓包的风险而且现在 HTTPS 证书成本几乎为零没有理由不开。3.2 SameSite 属性与跨站请求的博弈SameSite是近几年浏览器重点推的属性用来缓解 CSRF 攻击。它有三个值Strict、Lax、None。Strict最严格任何跨站请求都不带 Cookie。用户体验上会有问题比如从别的网站点链接跳过来登录态带不上会显示未登录。Lax折中方案GET 导航请求比如点链接跳转会带 CookiePOST 跨站请求不带。这是目前主流浏览器的默认值。None跨站请求也带 Cookie但必须同时设置Secure否则浏览器会拒绝。Django 2.1 之后支持SESSION_COOKIE_SAMESITE配置。我的建议是如果你的系统有前后端分离、跨域调用的需求通常需要设成None并配合Secure和 HTTPS。如果是传统的同站应用用Lax就够了安全性更好。这里有个容易踩的坑设成None但忘了开Secure浏览器会直接忽略这个 Cookie表现就是“登录接口返回成功但后续请求就是没登录态”排查起来很费时间。3.3 域名与路径配置的细节SESSION_COOKIE_DOMAIN和SESSION_COOKIE_PATH这两个配置在单域名单路径的应用里基本不用管但一旦涉及子域名共享登录态就必须搞清楚。比如你有www.example.com和api.example.com希望登录态在两个子域之间共享就要把SESSION_COOKIE_DOMAIN设成.example.com前面带点表示包含所有子域。如果不设Cookie 默认只对当前域名有效跨子域就带不上。SESSION_COOKIE_PATH默认是/表示全站有效。如果你把 Django 部署在某个子路径下比如/myapp/而这个配置没改Cookie 的 path 还是/虽然通常也能工作但在某些反向代理配置下会出现路径不匹配导致 Cookie 不发送的问题。我一般建议保持默认的/除非有明确的隔离需求。4. 登录态设计中的几个关键决策4.1 登录时该往 Session 里放什么很多教程里登录逻辑就是request.session[user_id] user.id然后每个需要登录的接口去取这个 user_id。这个做法能用但不够好。我的习惯是往 Session 里存一个精简的用户标识然后在中间件或装饰器里统一做校验和用户对象加载。具体来说Session 里只存user_id或者一个不可猜测的 token不要存整个用户对象。原因有两个一是 Session 数据每次请求都要反序列化存太多字段影响性能二是用户信息可能变化比如改了昵称、权限存在 Session 里的旧数据会导致不一致。正确做法是每次请求根据 user_id 去数据库或缓存里取最新的用户信息。Django 自带的django.contrib.auth其实已经帮你做好了这套逻辑login()函数会把用户 ID 和认证后端信息写进 SessionAuthenticationMiddleware会在每次请求时把request.user挂上。如果你在用 Django 的认证系统直接用它就好不要自己造轮子。但如果你在做自定义的登录态比如多端登录、第三方登录就需要自己设计 Session 里存什么。4.2 Session 过期与续期策略SESSION_COOKIE_AGE控制 Cookie 的过期时间SESSION_EXPIRE_AT_BROWSER_CLOSE控制是否在浏览器关闭时失效。默认SESSION_EXPIRE_AT_BROWSER_CLOSE False也就是 Cookie 会一直保留到SESSION_COOKIE_AGE到期。这里有个很多人不知道的机制Django 默认会在每次请求时如果 Session 被修改了就更新过期时间如果没修改过期时间不变。这意味着如果用户一直有操作Session 会不断续期不会掉线。但如果用户只是浏览不产生写操作Session 到期时间不会延长。如果你希望“用户只要在活动就一直保持登录”可以设置SESSION_SAVE_EVERY_REQUEST True这样每次请求都会更新 Session 的过期时间。代价是每次请求都要写一次 Session 存储对数据库或缓存的压力会明显增加。我的建议是对性能敏感的系统不要开这个而是通过前端定时心跳请求来触发续期把续期的频率控制住。4.3 多端登录与 Session 冲突一个账号在多个设备登录是共享一个 Session 还是各自独立这取决于你的业务需求。Django 默认的行为是每次调用login()都会生成新的 session_key如果同一个浏览器再次登录旧的 Session 记录会被覆盖实际上是新生成一个旧的还在存储里直到过期。如果你需要“同一账号只能在一个设备登录”就要在登录时主动清理该用户的其他 Session。实现方式是在 Session 里存 user_id 的同时维护一个“用户当前有效 session_key”的记录登录时把旧的删掉。这个逻辑 Django 没有内置需要自己写。反过来如果你需要“多端同时在线”那就什么都不用做让每个设备各自持有自己的 sessionid 即可。我做过一个项目需求是“账号在 A 设备登录后B 设备再登录A 设备要被踢下线”。实现思路是在用户表或缓存里存一个current_session_key每次请求校验当前 session_key 是否等于这个值不等就强制登出。这个方案简单有效但要注意并发场景下的竞态问题。5. 那些年我踩过的 Cookie 与 Session 坑5.1 跨域场景下 Cookie 带不上的完整排查前后端分离项目里“登录接口返回 200但下一个接口就 401”是最常见的问题。排查这个问题的链路我总结成下面几步按顺序走基本能定位看响应头有没有 Set-Cookie。如果登录接口的响应里根本没有Set-Cookie说明后端没写 Session检查登录逻辑是否真的调用了login()或写了request.session。看 Set-Cookie 的属性。重点看SameSite和Secure。如果SameSiteNone但没Secure浏览器会丢弃。如果Secure开了但你在用 HTTP也会丢。看请求头有没有带 Cookie。如果响应有 Set-Cookie 但后续请求没带检查withCredentials前端 axios 需要设withCredentials: true和 CORS 配置里的CORS_ALLOW_CREDENTIALS。看 Cookie 的 Domain 和 Path。跨子域时 Domain 要设对Path 不匹配也会导致不发送。看浏览器是否真的存了 Cookie。开发者工具的 Application 面板里能看到当前域名下的所有 Cookie如果那里没有就是前面某一步被浏览器拒了。这个排查顺序的价值在于它把“Cookie 带不上”这个模糊问题拆成了可验证的步骤每一步都有明确的观察点不用靠猜。5.2 Session 数据莫名丢失的几种真实原因除了前面提到的定时清理任务误删我还遇到过几种 Session 丢失的情况第一种是多进程部署但用了本地内存缓存。Django 默认的LocMemCache是进程内的多 worker 之间不共享。用户第一次请求打到 worker ASession 存在 A 的内存里第二次请求打到 worker BB 的内存里没有就掉线了。解决办法是换成 Redis 这类共享缓存。第二种是Session 序列化失败。Django 默认用 JSON 序列化 Session 数据如果你往 Session 里存了不能 JSON 序列化的对象比如 datetime、自定义类实例写入时会报错但有时候错误被吞掉了表现就是 Session 没存上。我一般会在 Session 里只存字符串、数字、列表、字典这些基础类型。第三种是缓存被主动清空。Redis 如果配置了内存淘汰策略在内存紧张时会淘汰 keySession 就可能被淘汰掉。用cached_db能缓解这个问题因为数据库里还有备份。5.3 Cookie 被篡改与签名校验Django 的 Session Cookie 本身是随机字符串不含数据所以篡改 sessionid 只会导致查不到对应 Session不会造成数据泄露。但如果你用的是signed_cookies引擎Cookie 里存的是签名后的数据Django 会用SECRET_KEY校验签名篡改后校验失败Session 会被视为无效。这里的关键是SECRET_KEY的保护。如果SECRET_KEY泄露攻击者可以伪造任意 Session。生产环境的SECRET_KEY必须从环境变量读取不能硬编码在代码里更不能提交到代码仓库。我见过有项目把SECRET_KEY写在 settings.py 里然后传到了公开仓库这是非常危险的。另外SECRET_KEY一旦更换所有已签名的 Cookie 和 Session 都会失效用户全部掉线。所以更换SECRET_KEY要选在低峰期并提前通知用户。6. 从请求到响应的完整链路复盘6.1 一次带登录态的请求经历了什么把整个链路串起来看一次带登录态的请求大致经历这些步骤浏览器发起请求自动带上当前域名下所有未过期的 Cookie其中包括 sessionid。请求到达 DjangoSessionMiddleware从 Cookie 里取出 sessionid。SessionMiddleware根据配置的SESSION_ENGINE去对应的存储里查这个 session_key 对应的数据。查到数据后反序列化成字典挂到request.session上。视图函数里通过request.session读写数据。如果 Session 被修改SessionMiddleware在响应阶段把新数据写回存储并可能更新 Cookie 的过期时间。响应返回浏览器如果 Session 有变化响应头里会带Set-Cookie更新 sessionid。理解这个链路后任何登录态问题都可以定位到具体环节是 Cookie 没带上步骤 1还是存储里查不到步骤 3还是写入失败步骤 6。6.2 中间件顺序对 Session 的影响SessionMiddleware在 Django 的MIDDLEWARE配置里是有顺序要求的。它必须排在AuthenticationMiddleware之前因为后者依赖前者提供的request.session。如果你调整了中间件顺序把AuthenticationMiddleware放到了SessionMiddleware前面访问request.user时会报错。另外如果你用了CsrfViewMiddleware它也需要在SessionMiddleware之后因为 CSRF token 的校验和 Session 有关联。这些顺序在 Django 默认生成的 settings 里是正确的但如果你手动增删中间件很容易打乱。我的习惯是除非有明确需求不动默认的中间件顺序。6.3 如何验证你的 Session 配置真的生效了配置改完之后怎么确认真的生效了我一般用这几个方法验证在视图里打印request.session.session_key和request.session.get_expiry_age()确认 Session 存在且过期时间符合预期。用curl -i请求登录接口观察响应头里的Set-Cookie确认属性HttpOnly、Secure、SameSite、Domain、Path都正确。直接连上 Redis 或数据库查看 Session 记录是否真的写进去了数据内容是否符合预期。模拟多进程场景用不同的 worker 处理连续请求确认 Session 能跨进程读取。这几步做完基本能确认配置没问题。不要只靠“页面上能登录”来判断那只能说明最基础的链路通了很多隐患在低负载下不会暴露。7. 一些容易被忽略的进阶细节7.1 Session 清理任务的正确配置方式clearsessions命令清理的是数据库中已过期的 Session 记录。注意它只对数据库和cached_db引擎有效对纯缓存和 signed_cookies 无效缓存的过期由缓存自身处理signed_cookies 由浏览器处理。配置定时任务时频率不用太高每天一次足够。命令是python manage.py clearsessions。如果你用的是cached_db清理数据库的同时缓存里的过期数据会由缓存自身的 TTL 机制处理不用额外操心。有个细节clearsessions清理的是expire_date已经过去的记录。如果你的SESSION_COOKIE_AGE设得很长比如一个月那这些记录会在数据库里躺一个月才被清理。所以SESSION_COOKIE_AGE的设置要结合业务需求和存储成本来权衡不是越长越好。7.2 用 Session 做验证码和临时状态存储除了登录态Session 还常被用来存验证码、表单临时数据、多步流程的中间状态。这些场景下Session 的生命周期通常很短用完就该删。我的做法是验证码这类数据存进 Session 后校验完立即del request.session[captcha]避免残留。多步表单的中间数据在最后一步提交成功后统一清理。如果不主动清理这些数据会一直占着 Session 存储直到 Session 整体过期既浪费空间也可能造成数据串用的问题。另外验证码这类场景对时效性要求高可以在存的时候额外记一个时间戳校验时判断是否超时而不是完全依赖 Session 的整体过期时间。7.3 分布式部署下的 Session 一致性多机部署时Session 存储必须是所有机器都能访问的共享存储。数据库和 Redis 都满足这个条件本地内存缓存不满足。这是选型时的硬性约束。如果你的系统用了负载均衡还要注意会话保持session affinity的配置。有些负载均衡器支持把同一用户的请求固定转发到同一台机器这样即使 Session 存在本地也能工作。但我不推荐依赖会话保持因为它会降低负载均衡的效果而且一旦某台机器挂了上面的用户全部掉线。正确的做法还是用共享存储让 Session 与机器无关。Redis 做主从或集群时要注意 Session 的读写一致性。如果用了 Redis 集群session_key 会分散在不同节点上这没问题因为每个 key 都是独立读写的。但如果主从同步有延迟刚写入的 Session 可能在从节点上读不到导致短暂的“登录后立即请求掉线”。这种情况可以通过读写都走主节点来规避代价是主节点压力大一些。8. 写在最后的一点个人习惯我在每个 Django 项目里都会在 settings 里显式地把 Session 相关的配置全部写出来哪怕有些值和默认值一样。这样做的好处是后来接手的人一眼就能看到这个项目的 Session 是怎么配的不用去翻 Django 文档确认默认值。我通常会把SESSION_ENGINE、SESSION_CACHE_ALIAS、SESSION_COOKIE_AGE、SESSION_COOKIE_HTTPONLY、SESSION_COOKIE_SECURE、SESSION_COOKIE_SAMESITE、SESSION_COOKIE_DOMAIN这几个集中放在一起加一段注释说明选型理由。还有一个习惯是新项目上线前我一定会用浏览器的开发者工具完整走一遍登录、操作、登出的流程逐个检查 Cookie 的属性和 Session 的存储记录。这个检查花不了十分钟但能挡掉大部分登录态相关的线上问题。毕竟登录态这种东西出问题的时候往往是全量用户受影响排查和修复的成本远高于上线前多看一眼。