Django Cookie与Session机制详解:存储引擎选型与登录态设计
2026/9/19 2:29:44 网站建设 项目流程

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 对应着“张三,今天入园,已玩过三个项目”。手环本身不包含你的身份信息,它只是一个凭证;真正的信息在后台的记录表里。

对应到 Django:

  • Cookie是浏览器端存储的一小段键值对,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_ENGINESESSION_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.cache',Session 数据存在你配置的缓存后端里,最常见的是 Redis 或 Memcached。读写都在内存,速度比数据库快一个数量级,适合高并发、对登录态读取频繁的场景。

这里有个关键选择:cachecached_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 的安全属性里,HttpOnlySecure是最基础的两个。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 攻击。它有三个值:StrictLaxNone

  • Strict:最严格,任何跨站请求都不带 Cookie。用户体验上会有问题,比如从别的网站点链接跳过来,登录态带不上,会显示未登录。
  • Lax:折中方案,GET 导航请求(比如点链接跳转)会带 Cookie,POST 跨站请求不带。这是目前主流浏览器的默认值。
  • None:跨站请求也带 Cookie,但必须同时设置Secure,否则浏览器会拒绝。

Django 2.1 之后支持SESSION_COOKIE_SAMESITE配置。我的建议是,如果你的系统有前后端分离、跨域调用的需求,通常需要设成None并配合Secure和 HTTPS。如果是传统的同站应用,用Lax就够了,安全性更好。这里有个容易踩的坑:设成None但忘了开Secure,浏览器会直接忽略这个 Cookie,表现就是“登录接口返回成功,但后续请求就是没登录态”,排查起来很费时间。

3.3 域名与路径配置的细节

SESSION_COOKIE_DOMAINSESSION_COOKIE_PATH这两个配置,在单域名单路径的应用里基本不用管,但一旦涉及子域名共享登录态,就必须搞清楚。

比如你有www.example.comapi.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 和认证后端信息写进 Session,AuthenticationMiddleware会在每次请求时把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”是最常见的问题。排查这个问题的链路我总结成下面几步,按顺序走基本能定位:

  1. 看响应头有没有 Set-Cookie。如果登录接口的响应里根本没有Set-Cookie,说明后端没写 Session,检查登录逻辑是否真的调用了login()或写了request.session
  2. 看 Set-Cookie 的属性。重点看SameSiteSecure。如果SameSite=None但没Secure,浏览器会丢弃。如果Secure开了但你在用 HTTP,也会丢。
  3. 看请求头有没有带 Cookie。如果响应有 Set-Cookie 但后续请求没带,检查withCredentials(前端 axios 需要设withCredentials: true)和 CORS 配置里的CORS_ALLOW_CREDENTIALS
  4. 看 Cookie 的 Domain 和 Path。跨子域时 Domain 要设对,Path 不匹配也会导致不发送。
  5. 看浏览器是否真的存了 Cookie。开发者工具的 Application 面板里能看到当前域名下的所有 Cookie,如果那里没有,就是前面某一步被浏览器拒了。

这个排查顺序的价值在于,它把“Cookie 带不上”这个模糊问题拆成了可验证的步骤,每一步都有明确的观察点,不用靠猜。

5.2 Session 数据莫名丢失的几种真实原因

除了前面提到的定时清理任务误删,我还遇到过几种 Session 丢失的情况:

第一种是多进程部署但用了本地内存缓存。Django 默认的LocMemCache是进程内的,多 worker 之间不共享。用户第一次请求打到 worker A,Session 存在 A 的内存里,第二次请求打到 worker B,B 的内存里没有,就掉线了。解决办法是换成 Redis 这类共享缓存。

第二种是Session 序列化失败。Django 默认用 JSON 序列化 Session 数据,如果你往 Session 里存了不能 JSON 序列化的对象(比如 datetime、自定义类实例),写入时会报错,但有时候错误被吞掉了,表现就是 Session 没存上。我一般会在 Session 里只存字符串、数字、列表、字典这些基础类型。

第三种是缓存被主动清空。Redis 如果配置了内存淘汰策略,在内存紧张时会淘汰 key,Session 就可能被淘汰掉。用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 一次带登录态的请求经历了什么

把整个链路串起来看,一次带登录态的请求大致经历这些步骤:

  1. 浏览器发起请求,自动带上当前域名下所有未过期的 Cookie,其中包括 sessionid。
  2. 请求到达 Django,SessionMiddleware从 Cookie 里取出 sessionid。
  3. SessionMiddleware根据配置的SESSION_ENGINE,去对应的存储里查这个 session_key 对应的数据。
  4. 查到数据后,反序列化成字典,挂到request.session上。
  5. 视图函数里通过request.session读写数据。
  6. 如果 Session 被修改,SessionMiddleware在响应阶段把新数据写回存储,并可能更新 Cookie 的过期时间。
  7. 响应返回浏览器,如果 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_keyrequest.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_ENGINESESSION_CACHE_ALIASSESSION_COOKIE_AGESESSION_COOKIE_HTTPONLYSESSION_COOKIE_SECURESESSION_COOKIE_SAMESITESESSION_COOKIE_DOMAIN这几个集中放在一起,加一段注释说明选型理由。

还有一个习惯是,新项目上线前,我一定会用浏览器的开发者工具完整走一遍登录、操作、登出的流程,逐个检查 Cookie 的属性和 Session 的存储记录。这个检查花不了十分钟,但能挡掉大部分登录态相关的线上问题。毕竟登录态这种东西,出问题的时候往往是全量用户受影响,排查和修复的成本远高于上线前多看一眼。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询