尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Cookie、Session、Token 详解:登录认证原理、选型与报错排查

Cookie、Session、Token 详解:登录认证原理、选型与报错排查 做了这么多年后端Cookie、Session、Token这三个词几乎每天都在打交道。面试的时候它们是高频题工作里它们是登录认证的基石可是真遇到线上问题——比如突然冒出一句there is no session with id或者第三方登录报token exchange failed——不少同学还是会卡壳。这篇文章我就把三者掰开揉碎讲一遍。不绕理论直接从原理、对比、实战选型、报错排查四个维度讲透最后附上我这些年踩出来的经验。无论你是刚接触Web开发的新人还是被各种认证报错折磨过的老手看完都能对登录认证体系有个完整的认知下次再遇到类似问题能直接定位方向。1. 三个概念先搞清楚它们分别是什么解决什么问题1.1 Cookie浏览器替你保管的便利贴Cookie 的本质是一小段文本数据由服务器通过Set-Cookie响应头下发给浏览器浏览器把它存下来之后每次请求同一个域名时会在请求头里自动带上。你可以把 Cookie 想象成贴在快递包裹上的便利贴。仓库服务器第一次发货时贴了一张纸上面写着这个包裹是发给谁的后续你再来寄件时仓库不用问你看便利贴就知道你是谁。这个机制解决的核心问题是HTTP 协议本身是无状态的服务器本来记不住你是谁你上次干了什么Cookie 让服务器有了记忆的载体。实际开发中Cookie 不止用于登录态。搜索历史、语言偏好、埋点标识比如_ga、_gid甚至部分电商的购物车都可能直接写在 Cookie 里。我见过很多团队用它做前端埋点参数传递把渠道来源、落地页参数都塞进 Cookie实现跨页面追踪这也是 Cookie 最常见的非认证用途。1.2 Session服务器侧的那本账Session 和 Cookie 经常成对出现但两者完全不同。Session 的数据存在服务器端客户端只保存一个凭证这个凭证通常就是 Session ID而这个 ID 大多数时候通过 Cookie 来传递。用个类比Session 是银行保险柜体系。你在银行开户创建 Session银行给你一把钥匙Session ID你的贵重物品存在银行服务器内存、Redis、数据库你每次来取东西都要出示钥匙银行工作人员通过钥匙找到对应的保险柜。钥匙丢了别人捡到就能冒领东西所以 Session ID 必须足够随机、足够难猜。所以 Session 解决了 Cookie 解决不了的问题敏感数据不能放在客户端。用户ID、角色、权限这些信息如果直接明文放 Cookie 里等于把家门钥匙贴在门上太危险。Session 把这些敏感数据留在服务端客户端只拿一个随机 ID安全边界明显清晰得多。1.3 Token一张自包含签名的通行证Token 是一串经过签名的字符串最常用的是 JWTJSON Web Token。JWT 的结构是Header.Payload.Signature三部分用点号连接Payload 里可以塞用户ID、过期时间、角色等声明信息Signature 由服务器用密钥对 Header 和 Payload 签名生成防止内容被篡改。Token 的思路像演唱会门票。门票上印着座位号、场次、有效时间票面上还有防伪码。入场时保安用机器验一下防伪验签不用打电话问售票系统无状态验过就直接放行。服务器看到 Token 时只需要用密钥验签就能确认这个请求确实是合法用户发来的不需要去数据库查 Session 记录天然适合分布式和微服务架构。1.4 一句本质对比靠什么记住你是谁这三个概念本质都在回答同一个问题服务器凭什么信任这个请求Cookie 本身不解决信任问题它只是数据的载体。承载什么内容取决于服务器怎么设计。Session 是服务端存储的会话数据客户端只保留一个不可预测的 ID信任建立在ID 猜不出来上。Token 是把信任关系放进一串自带签名的数据里服务器验签即可存储压力为零信任建立在签名无法伪造上。理解了这一层后面所有的对比、选型和报错排查都有基础了。2. 核心区别逐项拆解存储、生命周期、安全、性能与扩展2.1 存储位置与大小客户端 vs 服务端存储位置是三者的核心分水岭。Cookie 存在浏览器里容量很小单域名下通常只能存几十个每个最大约 4KB。Session 数据存在服务器表现为不同的存储介质单机场景存内存或文件分布式场景存 Redis、Memcached。Session ID 存在客户端 Cookie 里。Token 存在客户端由开发者决定放哪里localStorage、sessionStorage、内存变量或 Cookie 都行。Token 本身大小不受浏览器限制但随着塞入的声明增多字符串会越来越大放 Cookie 时容易挤占 4KB 空间放 localStorage 则要考虑 XSS 泄露风险。这里有个很实际的问题Session 服务端存数据每来一个用户就占一份内存。我维护过一台 4G 内存的老应用高峰期在线用户两三万Session 占的内存肉眼可见地涨。这是后来迁移到 Redis Session 的直接动力。而 Token 方案服务端零存储把所有压力转移到验签这个 CPU 操作上分布式下更省事。2.2 生命周期什么时候失效Cookie 有Expires和Max-Age两个属性控制过期时间。不设置的话Cookie 就是会话级 Cookie关掉浏览器就没了设置了过期时间则是持久化 Cookie。Session 也有超时时间常见默认 30 分钟。服务端会针对 Session 做空闲超时检测也就是说超时时间内没有任何请求Session 就会被回收。客户端关闭浏览器不会主动通知服务器删 Session只能等超时。Token 的过期时间由开发者写在 Payload 的exp字段里服务端验签时检查时间戳。问题在于还没到期的 Token 如果想提前作废怎么办纯 JWT 方案做不到因为它是无状态的服务器根本不记得签发了哪些 Token。所以实际项目里往往叠加维护一个黑名单或Token 版本号这就引入了额外状态。2.3 传输方式与安全边界Cookie 的传输是浏览器自动完成的。你设置了HttpOnlyJavaScript 就读不到它这对防 XSS 攻击非常有价值设置了Secure就只会在 HTTPS 连接上发送设置了SameSite就能限制跨站请求是否携带 Cookie对防 CSRF 攻击有帮助。Session ID 本质上也是走 Cookie 传输所以继承了上述安全属性。但 Session 方案天生有个软肋——CSRF。因为浏览器会自动带 Cookie攻击者只需诱导用户访问一个恶意页面这个页面向目标站点发请求就会自动携带 Session ID服务器没法区分这个请求是用户主动发起的还是攻击者伪造的。Token 的传输则通常放在请求头Authorization: Bearer token里。这种模式天然免疫 CSRF因为自定义请求头无法被跨域请求自动携带攻击者没法让浏览器帮你把这个 header 填上。但它怕 XSS——如果代码里有漏洞导致脚本能执行存在localStorage里的 Token 可以被直接偷走。所以很多安全团队推荐Token 放内存变量刷新页面后重新获取虽然体验略差但攻击面小了很多。2.4 性能与服务端压力Cookie 方案每次请求都带上大小有限性能损耗主要在流量上4KB 在大多数场景忽略不计。Session 方案每次请求都要拿着 Session ID 去服务端查。单机查内存快分布式查 Redis 就有一次网络 RTT如果 Redis 挂了整个登录体系就崩溃所以一般要做高可用。Token 方案验签不需要查库但签名和验签是有 CPU 开销的。尤其用非对称算法 RS256 时验签计算量比 HS256 大不少。另一个问题是 JWT 无法主动失效服务器想踢人下线、封禁账号都比较被动。更关键的是Session 天然是准确的用户改密码了、管理员封号了、用户退出登录了服务端只要删掉 Session立即生效。Token 做不到旧 Token 在到期前始终有效。所以涉及权限即时收回的场景Token 方案必须搭配黑名单或短过期时间。2.5 一张表直接看整体差异对比维度CookieSessionTokenJWT数据存储位置客户端浏览器服务端内存/Redis/DB客户端localStorage/内存/Cookie服务端存储开销无有按用户量线性增长无无状态传输方式请求头自动携带Session ID 走 Cookie通常放 Authorization 头大小限制单个约 4KB取决于服务端存储无硬性限制但越大越占带宽生命周期Expires/Max-Age 控制服务端超时回收exp 字段控制难主动提前失效典型安全风险XSS 读取、CSRF 携带Session ID 泄漏、CSRFXSS 读取、无法踢人、密钥泄露分布式扩展不涉及需要共享存储如 Redis天然支持水平扩展主动失效能力可删可改强删记录立即生效弱需黑名单或版本号辅助这张表基本就是三者的体检报告。日常怎么选看这张表就能得出方向。3. 实战选型具体场景到底用 CookieSession 还是 Token3.1 传统 MVC 服务端渲染Cookie Session 依然是最优选如果你做的是服务端渲染的网站页面由后端模板渲染前端几乎没有独立接口层那 Cookie Session 依然是最顺手也最安全的方案。原因在于浏览器自动带 Cookie登录成功后服务端redirect到首页后续请求全部自动携带会话开发节奏非常快。配套的中间件也非常成熟Java 的HttpSession、Python Flask 的session、Node Express 的express-session都是几行代码接入。再加上模板渲染场景下前端代码可控性强XSS 风险相对低HttpOnlyCookie 配合 CSRF Token 就构成了一个非常稳固的安全组合。我接手过一个老项目所有权限判断都从HttpSession里读当前登录用户。后来做服务升级唯一要处理的就是把 Session 从单机内存迁移到 Redis业务代码一行没改这体现了成熟生态的价值。3.2 前后端分离和移动端接口Token 是主流一旦前后端分离接口要同时服务 Web 和 AppToken 方案的优势就出来了。App 没有浏览器的 Cookie 自动管理机制如果硬套 Session还得手动维护 Cookie 的存取非常别扭。而 Token 就是一段普通字符串App 把它存到本地每次请求手动塞进 header完全自由。我做过一个跨 Web 和 App 的项目最终选了 JWT。流程是登录接口发放 Access Token短时效30 分钟和 Refresh Token长时效7 天接口校验时只验 Access Token过期后用 Refresh Token 换新 Access Token。这个设计同时解决无状态扩展和旧 Token 无法作废两个问题。前端拿到 Token 后不放 localStorage而是放内存变量刷新页面时用 Refresh Token 重新请求 Access Token。这样 XSS 攻击者拿不到长期有效的令牌Refresh Token 则存在 HttpOnly Cookie 里进一步减小泄露面。3.3 分布式与微服务Session 入 Redis 还是直接全上 JWT分布式场景的争论是经典话题。Session 方案的解法是会话共享把 Session 从各自内存里抽出来统一放 Redis。用户量再大Redis 集群也扛得住。Java 生态的 Spring Session 就是干这个的配置好之后所有节点共享一份会话数据登录一次任意节点都能识别。JWT 方案的解法是会话不共享但依然可信每个服务验签就行不需要访问中心化存储。横向扩节点的时候不需要为 Session 复制、失效、同步操心这在大规模微服务下很香。怎么选我个人的判断标准是系统内已有 Redis节点数不多团队对 Session 最熟那就 Session 入 Redis省心且能主动踢人。系统是微服务化、接口调用链长、节点动态伸缩频繁或者要对外开放 API 给第三方那就 JWT避免每次请求都命中共享存储成瓶颈。补充一个折中方案用 Session 存会话但通过 Spring Session 的 API 把会话数据序列化成 JWT 下发到前端服务端同时也存一份两边都校验。这样既能主动失效又能在离线时保活。缺点是复杂度上升除非有特殊需求一般不建议。3.4 JWT 实现登录态续签Refresh Token 与滑动过期Token 续签是实操中绕不开的环节。常见的机制是双 TokenAccess Token短时效比如 15 分钟到 2 小时用于业务接口鉴权。Refresh Token长时效比如 7 天到 30 天只用于/refresh接口换取新的 Access Token。刷新令牌时服务端校验 Refresh Token 本身有效再生成新的 Access Token同时可选地轮换 Refresh Token——也就是每刷新一次旧的 Refresh Token 立即作废前端拿到新的存起来。轮换的好处是一旦 Refresh Token 泄露攻击者最多用一次之后原合法用户再刷新就会暴露异常便于服务端感知并吊销整个会话。滑动过期则常用于 Session 或短期 Token只要用户持续操作就自动续期超过空闲时间才强制下线。体验比较好的是用户在编辑长文档时不会突然被踢缺点是攻击者如果一直持有 Cookie 或 Token 持续访问理论上也能无限续期需要配合异常检测。我实际项目的经验是登录时同时下发放 Access Token 和 Refresh Token前端只在内存里放 Access TokenRefresh Token 放 HttpOnly Cookie。刷新接口用 Cookie 里的 Refresh Token换回来后重新覆盖内存中的 Access Token。这套组合的体验和安全性比较均衡。3.5 第三方登录和开放平台JWT 几乎是事实标准还有一个很容易被忽略的场景第三方登录和开放平台。比如你接微信、Google 登录或者你作为平台方向第三方开放 APIOAuth 2.0 和 OIDC 的 ID Token 本身就是 JWT。第三方拿到的 token 要传到你的后端换网关签名 token这时候再做 Session 共享就不合适了JWT 自包含用户信息的特性非常适合这种跨系统信任传递。这就是为什么大量sign-in could not be completed token exchange failed类报错都出现在第三方登录环节——两个系统间的 token 换发是标准协议任何一个环节配置不对都会直接报错。4. 常见报错与排查实录线上认证问题一次讲透4.1 There is no session with idSession 为什么找不到这个报错非常典型英文全称类似There is no session with id [xxxx]多出现在 Spring、Hibernate 或某些 Java Web 框架中。表面意思是服务端根据请求里的 Session ID 查不到对应的 Session 数据。常见原因有三个Session 已被服务端回收。空闲超时到了或者服务器重启导致内存 Session 清空。Session ID 没传过来。客户端把 Cookie 禁了或者没有设置cookie支持跨域携带。多节点部署但 Session 没做共享。请求打到 A 节点创建的 Session下一次被负载均衡路由到 B 节点B 内存里根本没有这个 session。排查手段也很直接抓一下请求看 Cookie 里有没有JSESSIONID有的话去服务端看当前存活 Session 列表里有没有对应 ID确认多节点后看是否配置了 Spring Session Redis。绝大多数单向问题就出在这三步内。我遇到过最无语的一次是负载均衡配置了会话保持但超时时间设成了 2 分钟用户在高并发下被切到不同节点业务反馈一会儿登录一会儿掉线。定位到最后就是 Session 共享缺失而会话保持只是掩盖了问题。4.2 Token exchange failed第三方登录换 token 失败如何排查token exchange failed: token endpoint returned status 403 forbidden这类报错常出现在 OAuth/OIDC 流程中。流程是前端拿授权码后端拿授权码去 Token Endpoint 换 Access TokenToken Endpoint 返回 403换 token 失败。常见原因有这几种授权码已被使用过一次OAuth 的授权码是单次性的第二次兑换必然失败。client_id/client_secret错误或不匹配。回调地址和授权请求时的redirect_uri不一致这是非常容易被忽略的点。服务器系统时间和授权服务器相差过大JWT 里的iat签发时间、exp过期时间校验会导致请求被拒。部分服务对请求来源 IP 有地域限制导致返回 403这也是为什么海外服务的授权失败经常和country挂钩。排查这类问题先看授权服务器的原始响应体开发者通常会看到 JSON 里的error字段如invalid_grant、unauthorized_client比在浏览器控制台看一个笼统的失败信息有用得多。再核对回调地址、密钥、时间偏差基本能覆盖 80% 的 case。我自己的习惯是先用接口调试工具Postman 或 Reqable手动完成一次授权码换 token 的流程确认参数对再去查前端集成代码。这样能快速区分是业务代码问题还是配置问题。4.3 JWT 失效/刷新失败Access token could not be refreshed 类问题your access token could not be refreshed这种报错通常意味着前端拿 Refresh Token 去换新 Access Token 时失败了。可能的原因Refresh Token 过期这个只能重新登录。Refresh Token 被吊销常见于用户改密码、管理员封禁、或每次刷新轮换令牌策略下前端保管了旧令牌。服务端校验 Refresh Token 的签名密钥换了旧令牌直接无效。refresh_token字段为空字符串或者格式不对。我自己见过前端的拦截器把基本参数拼错了导致invalid refresh_token: empty string。根治办法是前端统一封装刷新逻辑遇到 401 时只触发一次刷新请求其他并发请求排队等待新 Token 再重放。刷新失败则直接清除本地登录态并跳转登录页。后端要对 Refresh Token 的吊销有完整的记录否则用户仍能用旧令牌反复刷新。4.4 Cookie 被篡改和伪造用 Burp 改 Cookie 之后发现能越权安全测试中常见的一个动作用 Burp Suite 拦截请求把 Cookie 里的用户 ID 从一个改成另一个然后放行如果后端直接信任 Cookie 里的明文用户 ID立刻就能越权访问他人数据。这个问题的根源是后端把标识和信任凭证混为一谈了。Cookie 只是载体没有签名验证。正确做法是要么用 Session 替换明文用户标识要么给 Cookie 值加签名比如签名成类似 JWT 的令牌。同时开启HttpOnly、Secure、SameSite三项防止 XSS 偷取和 CSRF 滥用。我再多说一句 XSS 和 Cookie/Taken 的关系XSS 攻击者能在页面里执行脚本就能读取localStorage里的 Token 或非 HttpOnly Cookie 里的会话。这也是为什么安全评审时我强烈要求登录态默认放 HttpOnly Cookie 或者内存变量业务代码再配合 CSP内容安全策略缩小脚本注入范围。4.5 调试工具的认证设置JMeter 与 Reqable 的实操细节压测或者接口联调时工具里的认证设置是很多人的坎。JMeter 里测登录态接口最直接的方式是用 HTTP Cookie Manager。添加之后第一个请求登录成功响应里的Set-Cookie会被自动管理后续请求自动携带。如果登录接口返回的校验码是放在响应体而不是Set-Cookie就要用后置处理器提取再用BeanShell或JSR223脚本手动写到 Cookie Manager。跑压测时每个线程组建议独立 Cookie 上下文避免线程间串号否则压测结果会出现大量 401。Reqable 这类调试工具里常见需求是把上一个接口响应里的 Cookie 带入下一个接口。操作思路是用脚本处理或者环境变量实现在第一个接口的响应脚本里解析Set-Cookie字段存到环境变量第二个接口的 Header 里引用这个环境变量。大多数接口工具的规则引擎都能做到关键是理解Cookie和Set-Cookie的格式差异一个是请求头一个是响应头别混淆。5. 几条实操心得关于认证设计我踩过的坑和最终建议做认证设计这么多年我的体会是没有银弹只有权衡。Cookie、Session、Token 不是互相替代的关系而是为了解决不同问题演化出的方案。第一能用 Session 的地方别硬上 JWT。如果你有运维 Redis 的能力且需要主动踢人、封号、权限即时回收Session 的模型和生态最省心。JWT 的无状态优势在中小系统里往往体现不出来反而因为想踢人踢不掉增加复杂度。第二Token 方案一定要处理续签和吊销否则上线后运营踢人下线需求一来就得返工。提前设计好 Access Token Refresh Token 的刷新链路配合令牌轮换和黑名单后端才不会被无限保留的旧令牌绑架。第三HttpOnly、Secure、SameSite 这三个属性在 Cookie 场景下能开就开。我在几个项目里提前把 SameSite 设为 Lax直接挡掉了一大批 CSRF 风险。Token 放在 localStorage 前先想清楚你的站点能不能在 XSS 面前完全免疫多数站点不能。第四报错信息真的很有价值。token exchange failed、no session with id、refresh token revoked这些字符串虽然让人头疼但排查路径基本都是固定的。把认证链路的时间校准、密钥治理、日志记录做好线上问题定位时间至少缩短一半。最后分享一个小技巧开发环境把 Session 和 Token 的过期时间调长到一天甚至更长避免调试过程中频繁重新登录但生产环境保持短有效期否则一旦泄露攻击者利用窗口会很大。可以在配置中心里动态调整这两套参数发布上线时报错率会明显下降。这套账号认证体系说复杂也复杂说简单也简单核心就是搞清楚信任凭证放在哪、怎么验、什么时候失效。把这三个问题想明白无论用哪套方案都不会跑偏。
返回列表