☰
Cookie与Session核心机制与实战:从验证码刷新到会话劫持防范
2026/10/2 2:50:31 网站建设 项目流程

前两天一个同事跑来问我,说他在 Django 里做了个图形验证码,生成之后塞进了request.session,但页面一刷新,验证码永远是同一个。他以为是浏览器缓存,折腾半天没解决。这问题很典型——没搞明白 Cookie 和 Session 的配合关系。其实验证码刷新不变,往往是浏览器没把 Set-Cookie 存下来、服务端拿不到 sessionId,或者 Session 里的 key 被覆盖。这一个小坑背后,就是 Cookie 与 Session 管理机制的核心问题。

作为一个常年跟 Web 请求、登录态打交道的人,我几乎每周都要和 Cookie、Session 碰面。这两个概念在网上被讲了无数遍,但很多文章要么太理论,要么只讲一遍概念就完了,真遇到生产环境的问题照样抓瞎。这篇文章我会从 HTTP 为什么需要它们讲起,一直讲到怎么用浏览器开发者工具看 Cookie、怎么排查数据库的 session timeout、怎么处理 Session 被第三方拿到的安全事故。适合刚入手的前后端开发、运维排查问题,也适合想搞清楚自己账号状态到底存在哪儿的普通用户。

1. Cookie 与 Session 到底是什么:先把概念掰开揉碎

1.1 HTTP 协议天生无状态,为什么需要会话管理

HTTP 协议本身是无状态的,意思是服务器默认不记得你上一次请求干了什么。举一个例子:你在电商网站登录了账号,然后往购物车里加了一件商品,如果服务器完全不记状态,你刷新页面的那一刻,它就得重新问一次“你是谁”。没有会话机制的话,你每看一个页面都得重新登录一遍,简直没法用。

所以需要一套“状态管理”方案,在多个请求之间建立起联系,让服务器能识别出“这一次请求还是刚才那个用户”。这个联系在技术上的通用叫法就是会话,而实现会话最常见的方式就是 Cookie 和 Session 配合使用。Cookie 负责在客户端保存身份凭证,Session 负责在服务端保存用户相关的状态数据。可以理解成:餐厅门口发手牌,你拿着手牌就可以去储物柜取东西,手牌本身不存你的物品,但服务员一看手牌就知道该开哪个柜子。

1.2 Cookie 的本质:浏览器里的小纸条

Cookie 是存储在浏览器端的键值对数据,由服务器通过响应头Set-Cookie下发,浏览器会按规则保存,并在后续请求同一个域名时自动带上。它最早是 Netscape 设计出来的,本意是让服务器能在客户端留下一个简单的标记。至于中文名,有人把它翻译成“小饼”“网络小甜饼”,日常交流基本没人用中文,大家直接说 Cookie 就行。

Cookie 有两个硬性限制:一是单个 Cookie 的体积极小,一般只有 4KB 左右,所以别指望往里塞大段数据;二是它存在客户端,用户自己能看到,甚至能改,除非你设置了 HttpOnly 属性让脚本无法读取。基于这两点,Cookie 适合保存不敏感、需要短期跨请求传递的标识信息,比如用户偏好、语言、主题色,以及最核心的 sessionId。

1.3 Session 的本质:服务器端的档案柜

Session 是服务端保存的状态数据结构,客户端只需要保存一个 sessionId。看一个标准流程:用户登录后,服务器在内存或 Redis 里创建一个 Session 记录,生成一个全局唯一的 sessionId,通过Set-Cookie: sessionId=xxx发给浏览器。之后浏览器每次请求自动带上这个 sessionId,服务器根据它找到对应的 Session,读里面的用户 ID、权限、购物车等数据。

Session 和 Cookie 最大的区别就在存储位置:Session 在服务端,Cookie 在客户端。这意味着 Session 相对安全,因为数据没有被用户直接握着;但 Session 有服务端存储成本,而且分布式部署时必须用 Redis 之类的共享存储,不然用户的请求被负载均衡切到另一台机器,就找不到原来的 Session 了。这也是很多小项目单机跑没问题、一上集群就反复掉登录的根源。

1.4 两者的配合流程

画个完整的时间线就清楚了:

  1. 浏览器第一次向服务器发起请求。
  2. 服务器处理请求时发现没有拿到 sessionId,于是新建一个 Session 对象,生成唯一 ID。
  3. 服务器在响应头里写Set-Cookie: JSESSIONID=xxxx; Path=/; HttpOnly。
  4. 浏览器保存这条 Cookie。
  5. 用户继续请求其他页面,浏览器自动在请求头带上Cookie: JSESSIONID=xxxx。
  6. 服务器从请求里取到 sessionId,再从存储中找到对应 Session,于是“认出”这个用户。

这套流程在服务端渲染的传统 Web 应用里非常自然,PHP、Java、Python 的框架基本都内置了类似机制。理解了它,后面看各种会话问题就能知道该从哪里下手。

2. 工作机制与核心细节:从创建到传输,必须搞懂的底层逻辑

2.1 Cookie 是在请求头里吗

直接回答:是。请求时浏览器会把 Cookie 放在请求头里的Cookie字段,多个 Cookie 用分号分隔;响应时服务器的Set-Cookie写在响应头里。看两个具体的报文:

# 浏览器发往服务器 GET / HTTP/1.1 Host: example.com Cookie: sessionId=abc123; theme=dark
# 服务器返回给浏览器 HTTP/1.1 200 OK Set-Cookie: sessionId=def456; Path=/; HttpOnly

这就是下班打卡的感觉:你进门时门禁发了一张卡(Set-Cookie),之后每次进出门都刷一下卡(Cookie 请求头),门禁系统才能认你。注意Set-Cookie是可以存在多个的,而且同一个响应里可以给不同的路径、域名设置各自的 Cookie。很多初学者在调试时会发现“明明我写了 Set-Cookie,但浏览器后续不携带”,这时候首先就要返回去检查请求头和响应头,看这条 Cookie 到底写没写成功、域和路径对不对。

2.2 Cookie 的属性和安全配置

Cookie 不是简单一个key=value,它有一堆控制属性,网上很多文章都不会细讲,但生产环境里出问题基本都是栽在这些属性上。

属性作用实际建议
Domain控制哪个域名携带这条 Cookie默认当前主域即可,别刻意扩大
Path控制哪些路径携带没特殊需求写成/
Max-Age / Expires过期时间,单位秒会话凭证越短越好
HttpOnly禁止 JavaScript 读取document.cookie凡是涉及 sessionId、Token 必须开
Secure只有 HTTPS 请求才发送生产环境必须开
SameSite控制跨站请求是否携带Lax 或 Strict,用于防 CSRF

为什么 HttpOnly 如此重要?设想一个留言板被注入了恶意脚本,如果 Cookie 没有 HttpOnly,脚本只要执行一行document.cookie就能偷走 sessionId,然后伪造成你发请求。攻击者拿着你的手牌进场,服务器完全认不出区别。开了 HttpOnly 之后,脚本只能干瞪眼,至少堵住了一条最常见的通道。

2.3 Session 的创建、过期与销毁机制

Session 并不是一建立请求就创建的,多数框架是等你真正调用 Session API 时才创建,比如 Django 里第一次访问request.session。以避免给每个匿名访客都分配一个无用的存储记录。

过期方面主要有两种策略:固定过期(Absolute Timeout)和滑动过期(Sliding Expiration)。固定过期就是不管你有没有活动,到点就删;滑动过期是你只要一直有操作,就不断往后顺延。多数 Web 容器默认是滑动过期,Tomcat 默认 30 分钟,Django 默认也有一套过期逻辑。生产环境里你应该主动配置,而且一定要设置上限,防止永不失效导致的僵尸 Session。

销毁也分两种:主动销毁是用户点了退出登录,服务端调用 session 清除接口;被动销毁是超过了超时时间,服务端在后续访问或定时任务里把它清理掉。有个容易忽略的点:服务端销毁 Session 之后,浏览器里的 sessionId Cookie 还在,下次请求照样带过来,服务端找不到对应 Session,又会新建一个。所以做退出登录时,除了清服务端数据,最好也把浏览器端 Cookie 删掉。

2.4 澄清一个误区:Cookie 不会让网站访问速度加快

热词里有个问题很有意思——“cookie 为什么不会让网站访问速度加快”。很多人把 Cookie 和缓存混为一谈,觉得浏览器存了东西就能加速。这是个很大的误解。Cookie 的定位是状态识别,不是缓存。就算你用 Cookie 保存了登录态,省去了每次输入账号密码,那也是“会话机制减少了重复认证”,不是浏览器资源加载变快。

而且反过来想,每次请求都要带上 Cookie,这些数据占用上行带宽。请求一个页面可能有一堆 Cookie,每个几十到一百字节,虽然单个不算多,但图片、接口、样式等几十个请求叠加起来还是有影响的。真正让页面加载变快的是 HTTP 缓存(Cache-Control、ETag)、CDN、压缩协议这些手段。所以正确的理解是:Cookie 不影响加载速度,配置不当还会成为负担,但它确实不是加速器。

3. 实操:从浏览器到服务端,Cookie 与 Session 的查看、导出与应用

3.1 开发者工具里怎么查看 Cookie,以及“Chrome 没有 Cookie”的原因

查看 Cookie 有两个入口。第一个是 Network 面板,选中任意请求,在 Headers 里看 Request Headers 里的Cookie字段,能看到这个请求实际携带了哪些 Cookie;如果响应里有Set-Cookie,也可以在这里确认服务器有没有下发新 Cookie。第二个是 Application 面板,左侧导航里找 Cookies,点开某个域名就能列出当前所有 Cookie 的 key、value、过期时间、HttpOnly 等属性。

如果你发现“Chrome 开发者工具里没有 Cookie”,先别急着怀疑工具坏了,大概率是这几种情况:页面根本没登录,自然没有会话 Cookie;请求是跨域的,目标域的 Cookie 不会显示在当前页面域名下;请求被 SameSite 阻止了,压根没带上;或者你在 Network 里开了过滤条件,比如只显示 XHR,没看到文档请求。解决建议:切到 Application 面板全量看一遍,或者清理过滤器后重新刷新页面。

3.2 360 浏览器怎么导出 Cookie

360 浏览器有兼容和极速两种模式,极速模式内核是 Chromium,操作和 Chrome 基本一致。F12 打开开发者工具,Network 面板里找一个目标请求,请求头Cookie字段整段复制出来,这就是最常见的“复制 Cookie”格式,很多爬虫脚本、接口工具都认这种格式。

如果想要更结构化的导出,推荐用浏览器扩展,比如 EditThisCookie,它能把当前站点的全部 Cookie 以 JSON 格式导出。使用路径很简单:登录目标网站,点扩展图标,选择 Export,把 JSON 复制走就行。导入功能也能用,但我要先说一句:Cookie 就是你的身份凭证,把它发给任何第三方,相当于把登录状态直接交代出去了。千万不要为了一时方便,把自己的会话信息传给不明来路的软件或人。

3.3 手机端 App 的 Cookie(夸克网盘、网易云)怎么获取

不少热词在问夸克网盘、网易云这类产品的 Cookie 在哪查看。其实原理都一样,只是载体不同。电脑网页版最好办,直接用开发者工具看。手机 App 就不一样了,一般需要用抓包工具,比如 Charles 或 Fiddler。大致的流程是:电脑和手机连同一个 Wi-Fi,手机设置代理指向电脑,安装并信任抓包工具的 CA 证书,然后打开 App 完成登录,再到抓包工具里找到登录后的某个请求,从请求头里拿出 Cookie。

拿网易云举例,如果你要做一些自动化脚本来同步歌单或获取私人内容,扫码登录后在开发者工具或抓包工具里能看到一个很长的请求头,里面含MUSIC_U等字段,那个就是会话凭证。夸克网盘也类似,网页版登录后 F12 直接看,App 端就得抓包。不管是哪个平台,我都要重复一遍:这种操作只建议用在自己账号和授权范围内,拿到 Cookie 不代表可以操作别人账号,平台协议和账号安全都经不起你乱来。

3.4 服务端怎么查看 Session,以及验证码保存 Session 的实例

服务端查看 Session 的方式取决于存储后端。Django 默认用了数据库表django_session,你可以直接查表里有哪些会话 key、什么时候过期;如果配了 Redis,就用 redis-cli 的keys session:*之类的指令去扫。Spring 生态则是查 Redis 里的 session key 或看日志、监控。

再说热词里提到的“第 1 关:生成验证码并保存 session”。图形验证码是 Session 的典型应用:服务端生成随机码,保存到 Session,再把图片返回给前端;用户提交时,服务端从 Session 里取答案比对。Django 里的核心代码如下:

import io import random from django.http import HttpResponse from django.views.decorators.cache import never_cache from PIL import Image, ImageDraw @never_cache def captcha(request): # 1. 生成随机验证码 code = ''.join(random.choices('0123456789ABCDEFGHJKLMNPQRSTUVWXYZ', k=4)) # 2. 存进 session,这一步是关键 request.session['captcha_text'] = code # 3. 生成图片 img = Image.new('RGB', (120, 40), color=(245, 245, 245)) draw = ImageDraw.Draw(img) for i in range(4): draw.text((10 + i * 25, 10), code[i], fill=(60, 60, 60)) buf = io.BytesIO() img.save(buf, format='PNG') return HttpResponse(buf.getvalue(), content_type='image/png')

这里有两个习惯我要强调:加@never_cache是为了防止浏览器把验证码图片缓存住,否则你刷新页面,图片还是旧的那张,用户会以为验证码没刷新;另一个是验证码用完之后立刻从 Session 里删掉,防止同一个验证码被重放多次。如果你发现“验证码刷新不变”,优先检查响应里有没有Set-Cookie,以及后续提交请求时浏览器有没有把 sessionId Cookie 带上。

4. 工程实战中的坑:Session 管理错误与安全事件应对

4.1 Hibernate “could not open hibernate session for transaction”排查思路

热词里那条很长的错误,ould not open hibernate session for transaction; nested exception is org.hibernate.exception...,在后端项目里非常常见。它的本质是 Spring 在开启事务时,无法从 SessionFactory 或连接池里拿到一个可用的会话连接。我建议不要被长堆栈吓住,直接看末尾的 Caused by,那里往往才是真正的根因。

常见的无非几类。第一,连接池满了。最典型的是老代码用完连接不释放,或者有慢查询把连接卡了很久。连接池的 maxActive 或 maxPoolSize 一旦耗尽,后续事务就开不了。第二,数据库连不上。数据库宕机、网络不通、账号密码错、防火墙拦截,都会导致拿连接失败。第三,SessionFactory 本身初始化失败,常见于实体映射错误、数据源配置错误。第四,事务边界问题,比如在事务外面访问懒加载属性,抛的是 LazyInitializationException,而不是这个,但排查思路是类似的。

我的排查习惯是:第一步看完整堆栈的 Caused by;第二步看连接池监控里 active 连接数和 waiting 数;第三步查数据库侧当前连接数、慢 SQL。一般这三步走完,错误原因就浮出水面了。另外提醒一句,连接池里建议加connectionTestQuery或者依赖框架自带的空闲连接探测,不然数据库主动断开空闲连接后,连接池毫不知情,下一次事务就会偶发报错。

4.2 OpenGauss 的 “session unused timeout. fatal: terminating connection” 怎么办

热词里还有一段数据库日志:opengauss=# \l warning: session unused timeout. fatal: terminating connectio。这其实是 OpenGauss 这类数据库服务端主动断开空闲会话的提示。它背后的逻辑很简单:一个数据库连接如果长时间不干活,白白占着内存和进程资源,服务端就会在参数控制的时间里把它踢掉,常见的参数包括session_timeout或云数据库侧的 idle 超时策略。

遇到这个错误,不要只想着去调数据库参数,更重要的是客户端和连接池要适应。连接池里躺着一条连接,数据库把它掐了,池子不知道,下次请求分到这条死连接就会报错。正确的做法是:

  1. 连接池的 maxLifetime 设置得比数据库的 session timeout 小一点,比如数据库 10 分钟断开空闲连接,连接池 8 分钟就把连接回收重建。
  2. 配置连接测试查询,HikariCP 的connectionTestQuery或validationQuery。
  3. 如果业务允许,再考虑调大数据库侧的session_timeout,但生产环境不要为了图省事直接设成 0,那等于关掉了保护机制。

4.3 验证码存 Session 的高并发与刷新问题

验证码虽然简单,但高并发下有许多不起眼的坑。比如每次刷新验证码都在写 Session,如果用的是本地内存 Session,用户基数一大,内存就直接飙升。建议配合 Redis 存储,并给每个验证码设置一个很短的过期时间,比如 5 分钟,防止垃圾 key 堆积。

另一个坑是“同一个浏览器标签页刷新验证码,新验证码把旧验证码覆盖了”。如果你的登录页允许用户刷新验证码,而后端只用一个captcha_text字段存,那确实一次只保留一个。这没问题,只要用户提交时取的是最后一次写入的值。但如果你开了多标签页登录,两个标签页互相覆盖,就会导致一个标签页输入的验证码永远校验不过。解决办法是把验证码和某个一次性请求标识绑定,或者干脆提醒用户在同一标签页完成登录。

还有一个容易被忽略的小点:生成随机码尽量用secrets而不是random,尤其当验证码是用于密码找回、支付确认这类高风险操作时,可预测的随机数等于没有随机。虽然是老生常谈,但业务代码里真的见到太多人贪方便用random了。

4.4 如果对方的 Session 被拿到,应该怎么操作

热词里那句“gpt 对方拿刀 session 后如何操作”,我看懂了,就是在问如果 Session 泄露或者被劫持,防御方该怎么做。先说原理:服务器认 sessionId 不认人,只要请求带上正确的 sessionId,它就当你是本人。所以攻击者一旦拿到你的 sessionId,无论通过 XSS、中间人抓包还是日志泄露,都能冒充你操作账号,这叫会话劫持。

如果你是开发者或者平台运营,发现会话可能被劫持,应急顺序是这样的:

  1. 立即让被攻击的 Session 失效。Django 里可以调用Session.objects.filter(session_key=xxx).delete(),或者在用户的退出接口里调用request.session.flush(),然后重新生成新的 sessionId。
  2. 强制用户重新登录,并对高危操作验二次凭证,比如短信验证码或支付密码。
  3. 当场检查所有相关 Cookie 的属性,确保 sessionId 是HttpOnly、Secure、SameSite=Strict/Lax。
  4. 开启异地登录提醒和风险告警,比如短时间内 IP 跨度大、设备变化等。
  5. 回溯泄露途径:是不是有 XSS 注入、有没有走明文 HTTP、日志里是不是打了 Cookie、有没有开发环境的 Session 配置被带到了生产。

需要特别声明:我这里只讲防御和补救。实际工作中也不要对未授权的目标做任何“验证”,那是法律红线。安全的逻辑是,宁可多设置几道边界,也别赌攻击者不会动手。

5. Cookie、Session 与 Token:三兄弟的对比与选型指南

5.1 一张表看懂三者的区别

现在前后端分离越来越普遍,Token 这个词出现的频率越来越高。很多人问“Cookie 和 Session 和 Token 到底怎么选”,我直接放一张对比表,看完基本就心里有数了。

维度CookieSessionToken
存储位置客户端浏览器服务端内存/Redis/DB客户端保存,服务端验签
实现方式标准 HTTP 头服务端维护状态加密/签名串,如 JWT
主要风险可被篡改、XSS 读取占用服务端存储、分布式要共享泄露后难以吊销,有效期控制麻烦
扩展性不涉及分布式需要共享存储天然无状态,适合微服务
典型用途保存偏好、会话 ID传统 Web 登录态前后端分离、开放 API、移动端

补充一点:Cookie 里也可以直接保存业务数据,比如购物车、主题色,但受 4KB 限制且有篡改风险;Session 适合服务端渲染和强状态业务;Token 适合 API、微服务和跨端。没有绝对的优劣,只有场景是否合适。

5.2 什么时候该用哪个

如果你做的是传统服务端渲染项目,比如 Django 模板、PHP、JSP 这种,Cookie + Session 是最省心的,框架自带,文档齐全,能覆盖绝大多数需求。前后端分离的 SPA 也可以用 Cookie,但要做跨域携带 CORS 的 withCredentials 配置,稍微麻烦。移动端多个 App、多个服务端的场景,Token 更灵活,因为客户端不用依赖 Cookie 域规则,只要请求头里带上Authorization就行。

我见过不少团队一上来就上 JWT,觉得“无状态很酷”。但实际上 JWT 的吊销是个麻烦事,真正的会话逻辑依然需要 Redis 维护一份黑名单,和 Session 没有本质区别。中小项目里,Cookie + Session 往往比 JWT 更稳、更好排查。不是说 JWT 不好,而是别盲目追新,先把你系统的扩展性想清楚再选。

5.3 Django 中同时使用 Cookie 与 Token 的设置方式

热词里有“django cookie 设置 token”,这里给一个很常见的实践。Django 默认的 Session 就是 Cookie 里存 sessionid。如果还要给 API 加一层 Token 认证,可以这样设置:

from django.http import HttpResponse def set_token_cookie(request, token): response = HttpResponse() response.set_cookie( 'auth_token', token, max_age=7 * 24 * 3600, httponly=True, secure=True, samesite='Lax', path='/' ) return response

把 Token 放进 HttpOnly Cookie,好处是前端 JavaScript 拿不到,XSS 窃取这条路被封了大半。但你也要接受它在跨域请求时的复杂度。另一种常见做法是把 Token 放在响应 JSON 里,前端存内存或按需使用,之后每次请求手动加Authorization: Token xxx,适合移动端和纯 API 场景。两种方案的取舍其实就是:Cookie 交给浏览器自动管理,Header 交给客户端手动管理。

6. 避坑清单与实操心得

6.1 常见问题速查表

我把这些年常被问到、也常让我自己头疼的问题汇总成一张速查表,方便你以后直接翻。

现象原因解决
页面刷新后登录态丢失Cookie 没保存、SameSite 拦截、过期时间太短看 Set-Cookie 和后续请求 Cookie 头,逐项排查
Chrome 开发者工具里没有 Cookie过滤条件、未登录、SameSite 阻止切 Application > Cookies,清过滤条件后刷新
360 浏览器怎么导出 CookieChromium 内核,和 Chrome 一致用 EditThisCookie 扩展,或复制请求头整段
Django 验证码刷新不变浏览器缓存、Session 没建立成功加 never_cache,检查 Set-Cookie 和 Cookie 回传
Hibernate 打不开 session连接池满、数据库不可用看 Caused by,查连接池和 DB 状态
数据库 session unused timeout服务端断开空闲连接连接池配探活,maxLifetime 小于数据库超时
Session 被第三方拿到泄露、XSS、抓包强制下线、改密、加 HttpOnly + Secure

6.2 几条红线经验

这些年踩过的坑不少,我总结成几条谁都要记住的底线:

  1. 会话 Cookie 必须开 HttpOnly,生产环境还要加 Secure。不要觉得“我们项目没这么重要”,攻击者从来不打声招呼。
  2. 不要把 sessionId 这样的敏感凭证存到 localStorage。localStorage 没有 HttpOnly 概念,任何 XSS 脚本都能读到,等于把钥匙摆在门口。
  3. 服务端 Session 一定要配置过期时间和容量上限。内存 Session 尤其危险,不设上限可能直接被拖垮。
  4. 绝对不要在日志里打印完整的 Cookie 或请求头。日志系统被攻破或者误发到第三方,等于把会话凭证直接送人。
  5. 分布式部署,Session 必须放到共享存储。别指望负载均衡的 sticky 策略能永远兜底,重启、扩缩容就会出问题。
  6. 把 Cookie 和 HTTP 缓存区分清楚。Cookie 是身份,缓存是速度,两者搞混会害你做出一堆错误优化。

6.3 最后再分享一个实用技巧

我个人遇到会话问题,从来不会一上来怀疑框架。我固定从 HTTP 层开始查:响应头有没有 Set-Cookie,后续请求有没有带 Cookie,服务端在不在 Session 里读到值。这三步走完,80% 的问题已经有眉目了。剩下的 20%,要么是分布式存储没同步,要么是客户端环境变化导致 Cookie 被清,也都能顺着这条线找到答案。

如果你需要从零设计一套会话系统,建议先把这几件事想清楚:Session 存哪里、过期时间用固定还是滑动、登录成功是否更换 sessionId、异常登录要不要告警、跨域 Cookie 怎么配。想明白了,你在任何框架里都只是换参数的问题。Cookie 和 Session 看起来简单,但它是所有 Web 应用的地基,把这套机制吃透,你排查其他鉴权问题会轻松非常多。

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

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

立即咨询