一文彻底搞懂 Cookie、Session、Token 登录认证机制 我先把话放这儿凡是能一篇文章把 Cookie、Session、Token 讲明白的人要么是在项目里被线上登录问题折磨过要么就是给新人擦过太多次屁股。这三个词几乎是后端面试必考题也是前后端联调时扯皮最多的地方。你如果现在还处于知道大概但说不透的状态这篇就是写给你的。我不打算堆概念而是从 HTTP 的无状态特性出发把三者的来龙去脉、应用场景、常见坑全部拆开揉碎。看完之后你不仅能跟别人讲清楚还知道自己项目里到底该用哪一个。1. 一切的起点HTTP凭什么不认识你1.1 无状态是什么状态HTTP 协议有一个根上的特性叫无状态很多人学的时候背了这句话但没真正体会到它有多无情。所谓无状态就是服务器对每一个请求都一视同仁它只根据你这次请求带过来的 URL、参数、请求头去决定响应什么不会记得你上一次请求做了什么。我打个比方你去同一家面馆第一次去吃老板记住了你喜欢吃宽面多加辣。这个记忆只存在于老板脑子里面馆的收银系统是不记录的。你第二天再去跟老板打个招呼他能想起来你是谁但如果你那个老板换成一个只看小票的收银员你在他眼里就是个全新的顾客。HTTP 就是那个只看小票的收银员。无论你连续发了多少次请求每次到达服务器时服务器都不知道这些请求是不是同一个人发出来的。这导致一个很直接的问题如果你在网站上登录了下一个请求进来服务器根本不知道你已经登录过。1.2 为什么所有登录方案都绕不开状态这两个字既然 HTTP 不认人又需要认人那就只能在协议之外想办法。这个想办法的过程本质上是做一件事在客户端和服务器之间建立一套标记身份的约定。这套约定需要满足三个基本要求第一服务器能区分不同用户第二客户端能保存并携带某种凭证第三服务器能验证凭证有效。围绕这三点业界演化出了两条主要路线一条是把凭证存在服务器上给客户端发一个取货码这就是 Session另一条是把凭证本身交给客户端服务器只负责校验这就是 Token。而 Cookie 则是浏览器提供的一种通用保存机制夹在中间做起了搬运工。理解了这一点后面所有内容都会变得特别顺。Cookie 解决的是凭证放在哪、怎么自动带过去的问题Session 解决的是服务器怎么记住用户的问题Token 解决的是怎么让凭证本身可信且不需要服务器存储的问题。三者不是竞争关系而是各司其职。2. Cookie偷懒在浏览器里的身份便签2.1 Cookie的本质Cookie 本质上就是一小段文本由服务器通过响应头 Set-Cookie 下发浏览器收到后存储在本地。等下次请求同一域名时浏览器会在请求头 Cookie 里自动带上它不需要前端写任何代码。我经常用一个比喻Cookie 是服务器贴在浏览器身上的便利贴。服务器想说记一下这件事就在响应里贴一张浏览器每次出门发请求都会主动把这些便利贴揣兜里带过去。整个过程对用户无感对前端代码也基本无感完全是浏览器层面的自动行为。它最早诞生的目的其实是解决电商场景的购物车问题。早期网站靠 Cookie 记住用户往购物车里放了什么后来才慢慢扩展到登录态、偏好设置、埋点追踪等领域。2.2 Cookie的出生、存活与归属一个 Cookie 包含的信息比很多人想象的多。除了最核心的键值对它还有 Domain、Path、Expires/Max-Age、HttpOnly、Secure、SameSite 等属性。这些属性决定了它能被谁看到、活多久、能不能被脚本读取。举个例子服务器下发登录凭证时通常会这么设置Set-Cookie: session_idabc123; Domain.example.com; Path/; HttpOnly; Secure; SameSiteLaxDomain 决定哪些域名能收到这个 Cookie。设置 .example.com 表示所有子域都能收到。Path 限定路径范围通常设为 / 让全站生效。Expires 是绝对过期时间Max-Age 是相对存活秒数两个都设时优先认 Max-Age。不设置过期时间的话这个 Cookie 就是会话级 Cookie浏览器一关就没了。这里有个特别容易踩的坑很多人以为 Cookie 是前端设置的其实大部分关键 Cookie 都是服务器通过 Set-Cookie 下发的。前端用 document.cookie 也能写但这种 Cookie 有很多限制而且很容易被 XSS 脚本读取所以真正重要的登录凭证尽量不要让前端碰。2.3 关于安全属性浏览器其实给了你三把锁Cookie 的存储方式决定了它天然有被窃取的风险所以浏览器提供了三道锁很多人只用了其中一把甚至一把都没用。第一把是 HttpOnly。设置了这个属性的 Cookie 无法被 JavaScript 的 document.cookie 读取只能随 HTTP 请求自动携带。即使页面被注入了恶意脚本攻击者也拿不到登录凭证。我在实际项目中会把所有跟身份认证相关的 Cookie 全部加上 HttpOnly这一条就能挡住绝大多数基于 XSS 的会话劫持。第二把是 Secure。它要求 Cookie 只能在 HTTPS 连接中传输HTTP 明文请求不会携带。这一点在本地开发时特别烦因为 localhost 有时候不认 Secure但上线之后它是必须的。第三把是 SameSite。它用来限制跨站请求时是否携带 Cookie取值为 Strict、Lax、None。Lax 是现在的主流默认值它可以防止跨站请求伪造攻击同时又不影响用户从外部链接进入网站时的基本体验。很多人忽略 SameSite直到发现支付回调或者第三方跳转登录失效才回过头来排查。还有一个前提单个 Domain 下的 Cookie 数量上限一般在 50 个左右单个 Cookie 的容量通常也就 4KB。别往里面塞大对象不然到后面只能看着新增字段写不进去干着急。我建议 Cookie 里只放标识符比如 sessionId、userId、语言偏好这些轻量字段其他数据走服务端存储。3. Session服务器记事本是怎么被认领的3.1 SessionID牵着客户端和服务器的线Session 的思路一句话就能概括重要的数据都存在服务器上客户端手里只拿一把钥匙。用户登录成功后服务器生成一个全局唯一的 ID这个 ID 就是 SessionID。服务器以它为 key把用户信息存进服务器内存或外部存储然后通过 Set-Cookie 把 SessionID 发给浏览器。浏览器之后每次请求都会自动带上这个 ID服务器拿到 ID 后去自己的记事本里查一下就知道是谁来了。这里的关键在于客户端手里没有用户信息只有一串毫无规律的随机字符串。真正有价值的资料全在服务器这边所以就算 SessionID 被偷了只要服务器做好校验和过期处理影响面也可控。流程拆开看是这样的用户提交用户名密码服务器核验通过。服务器生成一段随机字符串作为 SessionID把它连同用户资料一起写入服务端存储。服务器在响应头 Set-Cookie 里带上 SessionID要求浏览器保存。浏览器之后的每次请求自动带 Cookie 字段。服务器根据 SessionID 查到用户数据决定这次请求是否放行。3.2 服务端存储内存、文件还是RedisSession 数据存哪里是个值得掰扯的问题。新手最自然的选择是直接放服务器内存里比如 Java 的 HttpSession 默认就存内存。这种方式快是真快但有两个隐患一是内存大小有限Session 多了会造成压力二是服务器一重启所有会话全部失效用户集体掉线。稍微好一点的做法是把 Session 落盘到文件系统但这在分布式环境里几乎没有意义。真正比较通用的是用 Redis 这种外部缓存中间件来存 Session。SessionID 作为 key用户数据序列化后作为 value配合过期时间刚好能对上 Session 的生命周期。用 Redis 存 Session 还有一个好处天然支持过期淘汰。你给 key 设置一个 TTL时间到了自动删除不需要自己写定时任务去清理。在我参与过的项目里Session 的 key 一般设计成 session:{sessionId} 或者直接用 SessionID 本身value 存用户ID、角色、登录时间、最后活跃时间这些字段。TTL 设置成 30 分钟或 2 小时要看业务核心原则是让长时间不操作需要重新登录这个规则可控。3.3 Session在分布式架构下的命运一旦服务从单机变成多机Session 的麻烦就来了。你第一次请求落在 A 机器上Session 存在 A 的内存里下一次请求被负载均衡转发到 B 机器B 根本不知道你是谁直接让你重新登录。解决这个问题的办法有几种。最传统的是做负载均衡的会话粘滞让同一个用户的请求总部分配到同一台机器但这样就丧失了负载均衡的灵活性机器挂了用户还是丢状态。更常见的是 Session 数据统一放 Redis所有机器都去同一个 Redis 里查取这样就实现了会话共享。需要说明的是Session 共享不是没有成本。Redis 会多一次网络开销而且 Redis 挂了全站登录都受影响所以要考虑 Redis 的高可用。另一个方案是 Session 数据做持久化存储但那种高强一致的设计一般业务用不上没必要自找麻烦。4. Token把身份证明直接塞给客户端4.1 Token的核心思路让签名自己证明自己Session 方案有一个天然约束服务器必须保存状态。每次请求都要查一次存储分布式环境下还要共享存储这就催生了另一条路——Token。Token 的思路完全反过来我不在服务器存任何会话数据我把用户信息、过期时间塞进一个字符串里用密钥签名后发给客户端。之后每次请求客户端带上这个字符串服务器验一下签名如果数据没被篡改且没过期就放行。整个过程没有查库这一步所以服务端是无状态的。稍微类比一下Session 是你先在柜台上存一笔钱我发你一张存折每次来凭存折取钱还要当场核对账本Token 是我给你开一张防伪程度极高的现金支票谁拿支票谁就能花验的是支票本身真伪。4.2 JWT长什么样现在聊 Token绕不开 JWT。JWT 的全称是 JSON Web Token是目前应用最广的 Token 实现方案。一个 JWT 由三部分组成分别是 Header、Payload、Signature用点号分隔。一个典型的 JWT 长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c第一部分 Header 声明了签名算法和 token 类型第二部分 Payload 存放业务声明的字段比如用户 ID、角色、过期时间第三部分 Signature 是用服务端密钥对前两部分做的签名。任何人用同样的算法和密钥都能验证这个 JWT 是否被篡改过。重点是第三部分的校验逻辑。以常见的 HMAC-SHA256 为例服务端拿密钥把前两部分的拼接结果重新算一遍签名跟传来的签名做比对。一样就说明数据没被动过手脚再检查一下 exp 字段看看过期没有。整个过程不需要查数据库纯计算就可以完成这在微服务和分布式场景下特别友好。4.3 无状态不是没有代价JWT 看着很优雅但它有一个让很多人挠头的特性无法主动失效。Session 模式下你想把某个用户踢下线把服务端的 Session 数据一删就完事。JWT 不行它在客户端手上只要没过期服务器就算改了用户权限旧 token 依然能用。这也是为什么很多文章说JWT 是一把泼出去的水。所以要给 JWT 做踢人功能必须有额外手段。常见做法是把 JWT 的版本号或签发时间存进数据库或缓存每次请求校验时再比对一下。这样做等于把无状态又拉了回来属于用一点存储成本换可控性。另一个代价是 token 体积。JWT 比随机字符串长得多如果往 Payload 里塞太多业务字段可能动辄一两 KB。放在 Cookie 里会超限放在请求头里每次请求都会多发几百字节对移动端流量是一种浪费。所以 JWT 里的字段一般只放必要信息和其他系统共享的用户画像、权限大对象别往里面塞。5. 三兄弟同台竞技按场景对号入座5.1 一张表看透核心差异做一个直观的对比省得记混。维度CookieSessionToken数据存放位置浏览器本地服务器端客户端持有服务端校验服务器是否需要存储不需要需要通常不需要能否携带业务数据能但容量极小能存服务器随便存能但会膨胀 token 体积失效方式到时间自动失效删服务端数据即可只能等过期需额外手段抵御篡改能力弱需要额外签名强数据在服务器强靠签名算法分布式友好度不适用需要共享存储天然友好典型实现浏览器原生机制依赖容器或框架JWT 等这张表是我让很多新人存进备忘录的版本。核心记忆点就一句话数据放哪决定方案的性质。数据放浏览器就是 Cookie 路线数据放服务器就是 Session 路线数据本身自带验证能力就是 Token 路线。5.2 典型的选型场景不同业务有不同气质选型要跟着场景走。如果你做的是传统服务端渲染的网站比如用模板引擎直接输出页面那 Session 加 Cookie 是最顺手的组合。用户在页面上操作每个请求都走一遍服务端渲染SessionID 放 Cookie 里自动携带不需要额外处理跨域问题开发效率最高。这类场景我一般不推荐上 JWT因为没有必要。如果你是前后端分离的项目前端是独立的 SPA 应用数据接口通过 fetch 或 axios 调用那 Token 通常更舒服。尤其是多个前端共用同一套后端 API比如网页端加小程序端加 App 端Token 的跨域友好和平台无关优势就体现出来了。如果你在做微服务架构多个服务之间互相调用那无状态的 JWT 几乎是默认选择。服务 A 拿用户 Token 调服务 BB 自己验签就行不用去一个集中的 Session 服务查询链路短很多。5.3 混合使用才是常态实际项目很少是单纯一种方案。我刚接手过一个内部管理系统登录态用 Session 存在 Redis但某些接口之间做服务调用时又发了 JWT。一开始我也觉得这样不纯后来发现这恰恰是合理的。Session 负责管理用户会话存储权限和登录状态JWT 负责短期的服务间通信避免每次调用都查一次全局会话。让每套方案去做自己擅长的事比硬套一套纯正流派实用得多。我个人的建议是新人入门时先把三者原理搞透别急着站队。面试时可以讲清楚为什么这个项目选 Token 而不是 Session或者为什么服务端渲染项目还在用 Session这比背几个概念强得多。6. 实战中的选型与几个容易被坑的细节6.1 登录态设计里最常见的三个问题我帮人排查登录问题时发现很多灵异事件背后都是三个问题在循环。第一个问题是跨域请求不带凭证。前后端分离部署时前端在 a.com后端在 api.comaxios 请求默认不会携带目标域名的 Cookie。表现就是登录成功之后刷新页面又变成了未登录。解决方法是后端显式返回 Access-Control-Allow-Credentials 头前端请求也要开 withCredentials 选项。两个配置缺一不可缺一个就白搭。第二个问题是设置了 HttpOnly 之后前端代码以为登录态失效了。有些人习惯用 document.cookie 判断是否登录但 HttpOnly 的 Cookie 本来就无法被读取这时要用接口返回的数据或者说一个专门的登录态接口来判断。我见过团队因为这个问题绕了半天最后查出来是前端判断方式有误。第三个问题是 Token 过期时间设得太长。很多项目图省事把 JWT 设成 7 天甚至 30 天换来的是安全风险剧增。一旦 token 泄露攻击者很长一段时间内都是合法用户。比较稳妥的做法是把访问令牌设置成较短的过期时间比如 30 分钟再用一个刷新令牌去续期。这个思路后面细说。6.2 安全收尾HttpOnly、Secure和SameSite一个都不能少每次有人拿着安全测试报告来找我我发现大多数问题都出在 Cookie 属性设置不全。这不是技术难度的比拼纯粹是细节缺斤短两。我要求所有涉及认证的 Cookie 必须同时具备 HttpOnly、Secure、SameSiteLax 三个属性。HttpOnly 防止脚本读 Cookie这是对抗 XSS 的底线。Secure 保证 Cookie 只在 HTTPS 下传输防止明文网络被截获。SameSiteLax 能拦截绝大部分 CSRF 攻击又不影响正常的跨站跳转。如果是用 Spring 框架可以通过 Cookie 的配置统一设置如果是自己拼 Set-Cookie 头那就要手动把这些属性都写对。还有一个经常被问的问题Nginx 反代环境下后端没开 HTTPS但对外访问是 HTTPSSecure 属性可能导致本地调试永远登录不上。这种情况一般是让 Nginx 在转发时透传协议信息或者在网关层处理。6.3 过期时间、踢人逻辑和Token刷新最后聊聊生命周期设计这是一个实战里绕不开的细节。Session 的过期时间通常设成用户多久不操作才需要重新登录的阈值。电商网站可能是 30 分钟无操作就过期内部后台可能放宽到 2 小时。这个值不是拍脑袋定的要看业务容忍度。把过期时间做成滑动续期也是一种思路用户只要在操作就自动向后延长长时间不操作才失效。JWT 的过期就要多动点脑子了。如果只有单一 token过期时间设置成 2 小时还是 7 天都不完美。比较常见的做法是双 token 方案登录成功后签发短期访问 token比如 30 分钟有效。再加一个长期刷新 token有效期通常 7 到 14 天存在更安全的地方。访问 token 过期后前端拿刷新 token 调刷新接口换一个新的访问 token。刷新 token 过期了才让用户重新登录。这样做的好处很明显访问 token 泄露后只有 30 分钟的暴露窗口而刷新 token 可以单独做吊销。把刷新 token 存到服务器端并标记状态就能实现真正的踢人下线了。踢人逻辑的具体做法是每个用户维护一个 token 版本号用户修改密码或强制下线时把版本号递增旧 token 带了过期的版本号就直接拒绝。听上去绕但实现起来其实很直白也是我个人最推荐的方式。我自己的习惯是能简单就不炫技。单体应用老老实实用 Session别为了所谓的高级硬上 JWT前后端分离或有外部客户端接入再考虑 Token。技术在更新但你真正需要的永远是能扛住线上故障的方案能在出问题时十分钟内定位清楚的方案。别让一个登录机制变成你项目里最神秘的一块。