资讯动态

Django Cookie与Session深度解析:登录态管理、安全配置与性能优化实战

发布时间:2026/9/19 18:47:27 来源:尧图企业网站定制
1. 从一次登录掉线说起Django Cookie 与 Session 到底在干什么做 Django 项目的人几乎都遇到过这样的场景本地测试一切正常部署到服务器之后用户登录状态莫名其妙就丢了或者用户明明勾选了“记住我”第二天回来还是被踢到登录页再或者后台管理页面点几下就要求重新登录。这些问题表面上五花八门往深了查十有八九都落在 Cookie 和 Session 这两个东西上。Cookie 和 Session 是 Web 开发里最基础、也最容易被忽视的一对概念。说它基础是因为几乎每个框架都帮你封装好了你写request.session[user_id] 1就能用说它容易被忽视是因为一旦出问题很多人根本不知道从哪下手只能靠重启服务、清缓存、换浏览器这种“玄学”手段碰运气。这篇内容就是围绕 Django 里的 Cookie 和 Session 展开把它们的底层机制、配置参数、常见坑点、排查思路讲透。适合已经写过 Django 项目、但对登录态管理还是一知半解的人也适合正在做用户系统、权限控制、多端登录的开发者。看完之后你至少能做到三件事第一知道 Django 的 Session 到底存在哪、怎么存第二遇到登录态丢失能按步骤排查而不是瞎猜第三能根据业务场景选对 Cookie 和 Session 的配置策略。我自己的经验是Cookie 和 Session 这块知识光看文档很难形成直觉必须结合真实的踩坑场景才能记住。所以下面我会尽量用实际项目里的例子来讲而不是干巴巴地列 API。2. Cookie 与 Session 的底层逻辑为什么需要两个东西配合2.1 HTTP 是无状态的这是所有问题的起点要理解 Cookie 和 Session得先接受一个事实HTTP 协议本身是无状态的。什么意思就是服务器处理完一个请求之后就把这个请求忘得一干二净。下一个请求过来服务器根本不知道你是谁哪怕你上一秒刚登录过。这就像你去一家餐厅吃饭每次点菜服务员都换一个人而且新服务员完全不记得你之前点了什么。你要想让他知道你是老顾客就得每次点菜时主动出示一张会员卡。这张“会员卡”就是 Cookie。Cookie 是存在浏览器端的一小段文本浏览器每次向同一个域名发请求时会自动把对应的 Cookie 带上。服务器通过读取 Cookie 里的内容就能识别出请求来自哪个用户。但这里有个问题如果直接把用户信息比如用户 ID、用户名、权限全塞进 Cookie会非常不安全。因为 Cookie 存在用户自己的浏览器里用户可以随意查看和修改。你把is_admintrue写进 Cookie用户改一下就能变成管理员这显然不行。于是 Session 登场了。Session 的思路是敏感信息不放在浏览器而是存在服务器端浏览器只保存一个无意义的随机字符串也就是 Session ID。服务器拿到这个 ID去自己的存储里查出对应的用户数据。这样用户就算看到 Session ID也改不出什么花样来因为真正的数据在服务器手里。2.2 Cookie 和 Session 的分工与配合用一个生活化的类比Cookie 像是你手里的储物柜号码牌Session 像是柜子里的东西。号码牌本身没有价值丢了可以补办但柜子里的东西只有拿着正确号码牌的人才能取到。号码牌被人捡到确实有风险所以号码牌要设置有效期、要防止被偷看这就是 Cookie 安全配置的意义。在 Django 里这套机制被封装得很完整。默认情况下Django 使用数据库来存 Session 数据浏览器端只保存一个名为sessionid的 Cookie。用户登录时Django 在django_session表里插入一条记录把 Session ID 通过Set-Cookie响应头发给浏览器。之后每次请求浏览器带上sessionidDjango 中间件自动去数据库查把对应的数据加载到request.session里。这里有个关键点很多人不知道Django 的 Session 数据默认是经过签名和加密的。签名保证数据没被篡改加密保证数据没被偷看。所以即使有人拿到了 Session ID他也只能“冒用”这个会话而没法伪造出一个新的合法会话。这也是为什么 Session ID 泄露等同于账号被盗——因为服务器认的就是这个 ID。2.3 Django 中 Cookie 和 Session 的默认行为Django 的默认配置里有几个参数直接决定了 Cookie 和 Session 的行为很多人从来没改过但它们在生产和开发环境下的表现完全不同。SESSION_COOKIE_NAME默认是sessionid这是浏览器里看到的 Cookie 名字。SESSION_COOKIE_AGE默认是 1209600 秒也就是两周。SESSION_EXPIRE_AT_BROWSER_CLOSE默认是False意思是关闭浏览器后 Session 不会立即失效而是等到过期时间才失效。SESSION_SAVE_EVERY_REQUEST默认是False意思是每次请求不会刷新过期时间只有 Session 数据真正被修改时才刷新。这几个默认值组合起来就导致了一个常见现象用户登录后如果两周内没有任何操作Session 会过期但如果用户一直在操作Session 也不会自动续期因为SESSION_SAVE_EVERY_REQUEST是False。所以有些用户会遇到“用着用着突然要重新登录”的情况这往往就是 Session 过期时间到了。提示SESSION_SAVE_EVERY_REQUEST设为True会让每次请求都写一次 Session 存储对数据库压力不小。如果用的是数据库存储高并发场景下要慎重。3. Django Session 的存储方案怎么选数据库、缓存还是签名 Cookie3.1 五种存储引擎的适用场景Django 提供了五种 Session 存储方式通过SESSION_ENGINE配置切换。选哪种取决于你的项目规模、并发量和部署架构。存储引擎配置值数据位置适用场景数据库django.contrib.sessions.backends.db数据库表默认方案中小项目需要持久化缓存django.contrib.sessions.backends.cache缓存服务高并发能接受缓存丢失缓存数据库django.contrib.sessions.backends.cached_db缓存优先数据库兜底兼顾性能和可靠性签名 Cookiedjango.contrib.sessions.backends.signed_cookies浏览器 Cookie无服务端存储数据量小文件django.contrib.sessions.backends.file服务器文件特殊场景一般不推荐数据库方案是最稳的因为数据落在磁盘上服务重启、缓存清空都不影响。但它的缺点是每次请求都要查一次数据库并发高的时候django_session表会成为瓶颈。我见过一个日活几万的项目Session 表膨胀到几百万行查询慢得离谱后来换成cached_db才缓解。缓存方案性能最好但有个致命问题缓存服务重启或者内存淘汰时Session 数据会丢用户会被强制登出。如果你的缓存用的是 Redis 并且开了持久化这个问题会小一些但仍然不能完全避免。签名 Cookie 方案比较特殊它把 Session 数据直接加密后存在浏览器 Cookie 里服务端不存任何东西。好处是彻底无状态适合分布式部署坏处是数据量受 Cookie 大小限制一般 4KB而且每次请求都要传输这些数据带宽消耗大。另外签名 Cookie 的失效控制比较麻烦因为服务端没法主动让某个 Session 失效。3.2 数据库存储的清理问题用数据库存 Session绕不开的一个问题是过期数据清理。Django 提供了clearsessions管理命令可以删除过期的 Session 记录python manage.py clearsessions但这个命令不会自动执行你得自己配定时任务。很多项目上线后从来没跑过这个命令导致django_session表越来越大。我建议用系统的定时任务每天跑一次比如在 crontab 里加一行0 3 * * * cd /path/to/project /path/to/venv/bin/python manage.py clearsessions注意clearsessions只删除expire_date已经过期的记录。如果SESSION_COOKIE_AGE设得很大比如一年那这些记录会占着表一年才被清理。所以过期时间不要设得太离谱。3.3 缓存存储的配置细节如果决定用缓存存 Session配置大概是这样CACHES { default: { BACKEND: django.core.cache.backends.redis.RedisCache, LOCATION: redis://127.0.0.1:6379/1, } } SESSION_ENGINE django.contrib.sessions.backends.cache SESSION_CACHE_ALIAS default这里有个细节SESSION_CACHE_ALIAS指定用哪个缓存实例。如果你的项目里缓存有多个用途建议给 Session 单独分一个库或者单独一个 Redis 实例避免被其他缓存数据挤掉。Redis 的maxmemory-policy如果设成allkeys-lruSession 数据可能被优先淘汰用户就会莫名其妙掉线。这种情况我建议用volatile-lru或者给 Session 单独一个不淘汰的实例。4. Cookie 安全配置那些默认值在生产环境必须改4.1 HttpOnly、Secure 和 SameSite 三件套Django 的 Cookie 安全配置有几个关键参数默认值在开发环境下够用但生产环境必须调整。SESSION_COOKIE_HTTPONLY默认是True这个保持默认就好。它的作用是禁止 JavaScript 读取 Cookie能有效防止 XSS 攻击窃取 Session ID。如果你的前端需要通过 JS 读 Cookie那说明你的设计有问题应该改用其他方式传递数据。SESSION_COOKIE_SECURE默认是False生产环境必须改成True。这个参数控制 Cookie 是否只在 HTTPS 连接下发送。如果设成False用户在 HTTP 连接下也会带上 Session Cookie中间人就能截获。现在 HTTPS 已经是标配这个参数没有理由不开。SESSION_COOKIE_SAMESITE默认是Lax这个值在大多数场景下是合适的。它控制跨站请求时是否发送 Cookie。Lax模式下普通的链接跳转会带 Cookie但 POST 跨站请求不会带能防御大部分 CSRF 攻击。如果你的项目需要跨站携带 Cookie比如嵌入到 iframe 里那可能需要设成None但设成None时必须同时开Secure。SESSION_COOKIE_HTTPONLY True SESSION_COOKIE_SECURE True SESSION_COOKIE_SAMESITE Lax4.2 Cookie 域和路径的坑SESSION_COOKIE_DOMAIN默认是None表示 Cookie 只在当前域名下有效。如果你有多个子域名需要共享登录态比如www.example.com和api.example.com那需要设成.example.com。但这里有个坑设成.example.com之后所有子域名都能读到这个 Cookie包括一些你不希望共享的测试环境子域名。所以共享域要谨慎。SESSION_COOKIE_PATH默认是/表示整个站点都有效。一般不用改。但如果你有多个 Django 项目部署在同一域名下的不同路径比如/app1/和/app2/那就需要分别设置路径否则两个项目的 Session Cookie 会互相覆盖。4.3 自定义 Cookie 的注意事项除了 Session Cookie项目里经常还会自己设一些 Cookie比如记住用户名、主题偏好等。Django 提供了response.set_cookie()方法response.set_cookie( theme, dark, max_age3600 * 24 * 30, httponlyTrue, secureTrue, samesiteLax, )这里要特别注意自己设的 Cookie 默认httponly是Falsesecure也是False。如果不显式指定就会留下安全隐患。我见过不少项目在 Cookie 里存用户 ID 或者 token结果没设httponly被 XSS 一把梭。所以自定义 Cookie 时安全参数一个都不能省。提示如果 Cookie 里存的是敏感信息光靠httponly不够还得考虑加密。Django 的签名工具django.core.signing可以对 Cookie 值做签名防止篡改。5. 登录态实战从登录到登出的完整链路5.1 登录时 Session 是怎么写进去的Django 的登录函数django.contrib.auth.login()做了几件事首先验证用户身份然后把用户 ID 写进 Session接着轮换 Session ID最后把 Session 保存到存储后端。轮换 Session ID 这一步很关键它是为了防止 Session Fixation 攻击。所谓 Session Fixation就是攻击者先获取一个合法的 Session ID然后诱导受害者用这个 ID 登录。如果登录后 Session ID 不变攻击者就能用同一个 ID 访问受害者的账号。Django 在登录时调用cycle_key()生成新的 Session ID旧 ID 作废攻击者手里的 ID 就失效了。from django.contrib.auth import login def my_login_view(request): user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) # 此时 request.session 里已经有 _auth_user_id 等键 return redirect(home)登录后request.session里会多出几个键_auth_user_id、_auth_user_backend、_auth_user_hash。其中_auth_user_hash是用户密码的 HMAC用于在密码修改后让旧 Session 失效。这个设计很巧妙用户改密码后旧 Session 里的 hash 对不上就会被强制登出。5.2 登出时要做干净登出用django.contrib.auth.logout()from django.contrib.auth import logout def my_logout_view(request): logout(request) return redirect(login)logout()会做三件事清空当前 Session 数据、删除 Session Cookie、把request.user设为匿名用户。注意它清空的是当前 Session 的数据但不会删除数据库里的 Session 记录记录会在过期后被clearsessions清理。如果你希望登出时立即删除记录可以手动调用request.session.delete()。有个容易忽略的点如果用户在多个设备登录登出只影响当前设备的 Session其他设备的登录态不受影响。这是符合预期的因为每个设备有独立的 Session ID。如果你需要“登出所有设备”那得自己实现比如在用户表里存一个session_version字段登录时写进 Session校验时比对改密码或点“登出所有设备”时递增这个字段。5.3 手动操作 Session 的常见场景除了登录Session 还经常用来存一些临时数据比如购物车、表单草稿、验证码等。# 存 request.session[cart] {item1: 2, item2: 1} # 取 cart request.session.get(cart, {}) # 删单个键 del request.session[cart] # 判断存在 if cart in request.session: ... # 设置过期时间秒 request.session.set_expiry(3600) # 浏览器关闭即过期 request.session.set_expiry(0) # 永不过期慎用 request.session.set_expiry(None)这里有个坑request.session的修改不是立即写库的而是在响应返回时统一保存。如果你在视图里改了 Session但视图抛异常了那修改可能不会保存。另外如果同一个请求里多次修改 Session只有最后一次会生效。注意set_expiry(0)表示浏览器关闭就过期但这依赖浏览器的行为。有些浏览器有“恢复上次会话”功能关闭再打开可能还会保留 Cookie。所以不要把它当成绝对的安全措施。6. 常见问题排查登录态丢失、Session 表膨胀、跨域失效6.1 登录态丢失的排查清单登录态丢失是最常见的问题原因可能有很多。我整理了一个排查顺序按可能性从高到低现象可能原因排查方法部署后立即掉线SESSION_COOKIE_SECURETrue但站点是 HTTP检查站点协议或临时关掉 Secure多台服务器轮流掉线各服务器 Session 存储不共享检查是否用了本地数据库或本地文件一段时间后掉线Session 过期检查SESSION_COOKIE_AGE和SESSION_SAVE_EVERY_REQUEST特定浏览器掉线SameSite 或第三方 Cookie 限制检查浏览器控制台的 Cookie 警告改密码后掉线_auth_user_hash不匹配这是预期行为不是 bug重启服务后掉线用了缓存或内存存储换成数据库或持久化缓存排查时最有效的工具是浏览器开发者工具。打开 Network 面板看登录请求的响应头里有没有Set-Cookie看后续请求的请求头里有没有带上sessionid。如果登录响应没有Set-Cookie说明 Session 没保存成功如果后续请求没带sessionid说明 Cookie 被浏览器拒绝了可能是域名、路径、Secure、SameSite 配置有问题。6.2 Session 表膨胀的处理django_session表膨胀是数据库存储方案的常见问题。除了定时跑clearsessions还有几个优化点第一给expire_date字段加索引。Django 默认已经加了索引但如果你手动改过表结构要确认索引还在。第二如果表实在太大可以考虑分区或者定期归档。第三如果并发很高考虑换成cached_db让大部分读请求走缓存。我遇到过一个极端案例某项目django_session表有上千万行clearsessions跑一次要几十分钟还会锁表。后来改成每次只删一批用LIMIT分批删才解决。Django 的clearsessions不支持分批需要自己写脚本from django.contrib.sessions.models import Session from django.utils import timezone def clear_expired_sessions(batch_size1000): while True: expired Session.objects.filter(expire_date__lttimezone.now())[:batch_size] ids list(expired.values_list(pk, flatTrue)) if not ids: break Session.objects.filter(pk__inids).delete()6.3 跨域和子域名场景的 Session 共享前后端分离的项目里前端域名和后端域名不同Session Cookie 的跨域问题很常见。浏览器的规则是如果请求是跨域的默认不会带上 Cookie除非满足几个条件请求的credentials设为include服务端响应头里有Access-Control-Allow-Credentials: true并且Access-Control-Allow-Origin不能是*必须是具体域名。# Django 端配置 CORS_ALLOW_CREDENTIALS True CORS_ALLOWED_ORIGINS [https://frontend.example.com] SESSION_COOKIE_SAMESITE None SESSION_COOKIE_SECURE True注意SameSiteNone必须配合SecureTrue否则浏览器会拒绝这个 Cookie。而且现在很多浏览器对第三方 Cookie 有限制跨域 Session 越来越难做。如果条件允许尽量让前后端同域或者用 token 方案替代 Session。子域名共享 Session 相对简单设SESSION_COOKIE_DOMAIN .example.com就行。但要确保所有子域名都用同一个 Session 存储后端否则 Cookie 虽然共享了但服务器查不到对应的 Session 数据还是白搭。7. 几个容易被忽略的细节和我的实操心得7.1 Session 并发写入的问题Django 的 Session 默认没有并发控制。如果同一个用户同时发多个请求每个请求都修改 Session可能会出现覆盖。比如用户同时打开两个标签页一个加购物车一个改地址两个请求都读到了旧的 Session 数据然后各自写回后写的会覆盖先写的。这个问题在数据库存储下更明显因为数据库操作有延迟。解决办法有几个一是用select_for_update锁行但会影响性能二是把 Session 数据拆细不同请求改不同的键三是改用缓存存储缓存的原子操作能缓解一部分问题。我个人的经验是如果业务对并发写入敏感尽量不要把关键数据放 Session而是放数据库Session 只存用户 ID。7.2 Session ID 的熵和安全性Django 生成 Session ID 用的是secrets模块默认 32 个字符熵足够高暴力破解基本不可能。但如果你自己实现 Session 机制千万别用random或者时间戳那些都不安全。Session ID 必须是密码学安全的随机数。另外Session ID 在日志里可能会被记录下来。如果你的日志系统会记录请求头那 Session ID 就泄露了。建议在日志里过滤掉Cookie头或者只记录 Session ID 的前几位。这个细节很多项目都没注意但一旦日志泄露攻击者就能直接冒用登录态。7.3 移动端和 API 场景的取舍移动端 App 或者纯 API 项目用 Session 其实不太合适。因为移动端没有浏览器的 Cookie 机制得手动管理 Session ID还要处理过期、刷新等问题。这种场景更适合用 Token 方案比如 JWT 或者 DRF 的 TokenAuthentication。但 Token 也有自己的问题比如无法主动失效、续期麻烦。所以有些项目会用“Token Session”的混合方案Token 用于身份认证Session 用于存临时状态。具体怎么选取决于你的业务需求和安全要求。我的建议是如果团队对 Session 机制理解不深不要轻易上 Token因为 Token 的坑更多。7.4 一个真实项目的配置参考最后分享一个我在生产环境用过的配置项目是中等规模的 SaaS日活几千用的是数据库存储加 Redis 缓存# Session 配置 SESSION_ENGINE django.contrib.sessions.backends.cached_db SESSION_CACHE_ALIAS session SESSION_COOKIE_NAME sid SESSION_COOKIE_AGE 60 * 60 * 24 * 7 # 一周 SESSION_COOKIE_HTTPONLY True SESSION_COOKIE_SECURE True SESSION_COOKIE_SAMESITE Lax SESSION_SAVE_EVERY_REQUEST True SESSION_EXPIRE_AT_BROWSER_CLOSE False # 单独的 Session 缓存 CACHES { default: { BACKEND: django.core.cache.backends.redis.RedisCache, LOCATION: redis://127.0.0.1:6379/0, }, session: { BACKEND: django.core.cache.backends.redis.RedisCache, LOCATION: redis://127.0.0.1:6379/1, }, }SESSION_SAVE_EVERY_REQUEST True配合SESSION_COOKIE_AGE 一周实现了“滑动过期”用户只要一周内有过操作登录态就自动续期。这样既保证了安全性又不会让活跃用户频繁登录。代价是每次请求都写一次缓存和数据库但因为我们用了cached_db写操作先走缓存数据库写入是异步的压力可以接受。这个配置跑了两年多没出过登录态相关的线上事故。唯一需要注意的是 Redis 的内存监控因为 Session 数据会占一部分内存要确保不会把 Redis 撑爆。

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

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

免费获取报价