ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Cookie、Session、Token 一文讲透:从原理到实战排查

Cookie、Session、Token 一文讲透:从原理到实战排查 做 Web 开发这么多年几乎每个项目都会被问到cookie、session、token 到底有什么区别以前我习惯直接甩一段定义后来发现真正让人糊涂的不是概念本身而是这三样东西在浏览器、服务端、接口调用里到底怎么流转的。这篇文章把 cookie 机制、session 管理和 token 的关系一次讲透看完之后你应该能自己从零设计一套登录态也能知道 there is no session with id、token exchange failed 这类报错到底在说什么。先说一个最底层的背景HTTP 是无状态的。服务器处理完一个请求转头就忘了你是谁。cookie、session、token 本质上都是在给这个没记性的协议补状态。cookie 是浏览器端保存的一张小纸条session 是服务器端保存的一份档案token 是客户端自己保管的一张凭证。三兄弟经常被拿来对比但实际项目里往往不是三选一而是搭配着用。1. Cookie 是怎么在浏览器里住下来的1.1 Cookie 的完整通信过程Cookie 的中文直译是小甜饼但做 Web 的人更愿意把它叫成浏览器里的小便签。整个机制只有四步理解了这四步后面所有衍生问题都好解决。第一步你访问一个网站服务器在处理完请求后可以在响应头里加一个Set-Cookie字段。第二步浏览器看到这个字段后会按照 Domain、Path 等规则把内容存到本地。第三步以后你再次访问同域名、同路径下的接口时浏览器会自动把这些 Cookie 放到请求头的Cookie字段里发给服务器。第四步服务器读这个字段就能知道噢这是上一个请求来过的那个人。一个最简单的登录响应长这样HTTP/1.1 200 OK Content-Type: application/json Set-Cookie: session_id8f3a2c9e; Path/; HttpOnly; SameSiteLax {code:0,message:ok}浏览器看到Set-Cookie后会把session_id8f3a2c9e存起来。下次请求自动带上GET /api/profile HTTP/1.1 Host: example.com Cookie: session_id8f3a2c9e很多新手容易忽略一个点不是所有存储都叫 Cookie也不是所有状态都适合放 Cookie。Cookie 只是载体它里面放的是 session 的 ID、用户偏好、埋点信息还是签名后的身份凭证决定了整个系统的安全模型。1.2 关键属性配置错一个都可能出事故Cookie 不是简单的keyvalue它有一堆属性每个属性都对应一类问题。我列一张表你直接把这张表当成配置检查清单用属性作用踩坑点Domain控制哪些域名能收到该 Cookie配成.example.com会给所有子域发容易扩大攻击面Path限制 Cookie 发送的路径范围配成/最省事但意味着整站都会带Expires/Max-Age控制持久化时长不配就是会话级浏览器关了就没了Secure强制 HTTPS 才发送在线上环境不配等于裸奔HttpOnly禁止 JS 读取 Cookie不配的话XSS 一打一个准SameSite限制跨站请求是否携带 Cookie做跨域登录时最容易配错这里具体说两个最常见的坑。第一个是HttpOnly。它本身不加密 Cookie只是让浏览器的document.cookie读不到。只要这条 Cookie 只用来做身份认证就必须加HttpOnly否则页面里只要被注入一段脚本攻击者就能把 Cookie 偷走。这是 XSS 攻击之后最直接的盗号路径。第二个是SameSite。它解决的是 CSRF跨站请求伪造问题的一部分。SameSiteLax表示普通跨站链接可以带 Cookie但是跨站 POST 表单不能带SameSiteStrict更严格所有跨站都不带SameSiteNone允许跨站携带但必须同时设置Secure也就是说必须跑在 HTTPS 环境下。以前老项目没设 SameSite 也能跑是因为浏览器默认 Lax但如果你建的是跨域接口给第三方站点调用就需要针对性地测试。还有一个体感很明显的限制单个 Cookie 大小不能超过 4KB一个域名下通常不能超过 50 个。把用户资料、权限列表全塞 Cookie 的做法很快就到上限。所以我做这类方案时只把能定位用户身份的东西放 Cookie比如 session ID其他数据放服务端或请求体里。1.3 怎么在浏览器里找到和导出 Cookie你搜索夸克网盘 cookie 在哪里查看网易云 cookie 手机怎么获取360浏览器怎么导出 cookie本质上都是同一件事用浏览器开发者工具找登录态。以 Chromium 内核的浏览器为例按 F12 打开开发者工具切到 Application应用程序面板左侧找到 Cookies选中当前站点域名右边就会出现该域下所有 Cookie 的 Name、Value、Domain、Path、Expires 等列。你要的登录态通常是一个带session、token、auth、login字样的键。很多网盘、音乐网站的网页端登录态就是这么存的。360 安全浏览器、Edge 这些基本都是 Chromium 内核操作路径一模一样。手机端没有开发者工具的话就得靠抓包工具或者通过电脑端浏览器登录后拷贝 Cookie。需要说明的是直接改 Cookie 里的用户标识并不能提权因为合法后端不会信任明文 Cookie这是下面几节要展开讲的关键点。2. Session把状态留在服务器端2.1 Session 的工作流程和设计原因Cookie 方案最大的问题是数据在客户端手里不可控。你没法阻止用户去改、去删、去重放这些数据。所以传统 Web 应用的主流做法是把状态留在服务器端只给浏览器一个随机的钥匙这就是 Session。完整流程是这样的用户提交用户名密码服务器验证通过后在 Session Store 里生成一条记录比如user_id123然后返回一个随机生成的 Session ID通常是一个足够长的、不可预测的字符串通过Set-Cookie写进浏览器。浏览器之后每次都带着这个 Session ID服务器拿这个 ID 去 Session Store 查记录查到了就知道用户是谁。这里的关键点在于Session ID 本身不是用户身份它只是一把钥匙。用户身份、角色、购物车数据全部安全地躺在服务器侧。即使浏览器端的 Session ID 被人看到了攻击者也只是拿到了这把钥匙能不能用取决于有效期、IP 绑定等策略。用一个生活里的类比Session 就像酒店前台。你把行李用户数据交给前台保管前台给你一张房卡Session ID每次进门都刷房卡前台根据房卡找到你的行李。房卡丢了可以补办但是行李不会自己长腿跑掉。2.2 Session 应该存哪内存、Redis 还是数据库Session Store 是 Session 方案的心脏。最省事的是存在单机进程内存里比如 Tomcat、Express 默认的 MemoryStore。但这只适合个人项目因为第一进程重启 Session 全丢第二多实例部署时用户第一次请求打到 A 机器下一次被负载均衡分到 B 机器B 机器根本没有这份 Session。所以到了分布式阶段基本都用集中式存储Redis 是最常见的其次可以是数据库、Memcached。用 Redis 的好处是查询快、天然支持过期时间配合 TTL 能精确控制 Session 生命周期。伪代码大概是这样的结构key: session:{sessionId} value: { userId: 123, role: admin, loginAt: 1710000000 } ttl: 86400在后端代码里一个典型的 Node.js express-session Redis 配置长这样const session require(express-session); const RedisStore require(connect-redis)(session); app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, name: sid, resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: lax, secure: process.env.NODE_ENV production, maxAge: 24 * 60 * 60 * 1000 } }));name默认是connect.sid我习惯改成短一点的sid。saveUninitialized: false很重要它保证没登录的用户不会往 Session Store 里写垃圾记录否则爬虫一来 Redis 里全是空会话。2.3 Session 的常见坑丢失、并发写和命名误区Session 相关报错里最有名的就是There is no session with id [xxxxx]。很多人第一次看到会慌以为是用户登录态被黑。其实这个报错的意思是请求带了一个 Session ID但服务端在 Session Store 里查不到对应记录。原因无非三种Session 过期被 TTL 清掉了Redis 服务重启导致数据丢失Session ID 是伪造或篡改的。排查思路我会按顺序来先看 Redis 里还有没有那个 key没有就是过期或丢失再看客户端 Cookie 里的 Session ID 和 Redis key 是否一致不一致就是 Cookie Domain/Path 配错最后看服务端是不是重启过。很多时候用户登录后马上就掉线就是这个原因。另一个坑出现在 PHP 项目里报错是session file locked (timeout 60000ms)。PHP 默认把 Session 存文件文件锁是排他的如果同一用户的多个请求并发进来后面的请求会等前面的请求操作完要是某个接口执行太久就出现锁超时。这种问题要么把慢接口优化掉要么把 Session 换成 Redis我遇到后一般直接换 Redis一劳永逸。这里还要提醒一个概念误区Could not open Hibernate session for transaction里的 Hibernate Session 是数据库会话和 Web 用户的 Session 完全是两回事。一个是 ORM 用来做数据库事务的会话一个是保存用户登录状态的会话。很多新手在日志里看到 session 就以为是登录态出了问题实际上要看完整上下文。排查这类日志先确认到底是 Spring 的HttpSession、Hibernate 的SessionFactory还是 PHP 的$_SESSION别被同名术语带偏。3. Token把状态交给客户端保管3.1 Token 和 Session 的本质区别Session 方案是典型的服务端有状态Token 方案则走向另一个极端服务器不存储会话把用户身份信息经过签名加密后交给客户端保存。你用 Token 请求接口时请求头长这样Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMifQ.some-signature服务器收到后不需要查 Session Store只需要验证签名、看过期时间就能确定这个 Token 是哪个用户的。这种特性让它天然适合跨端场景App、小程序、第三方开放平台没有 Cookie 机制也能玩。浏览器端要手动把 Token 放到请求头不像 Cookie 那样浏览器自动携带。所以结论很清晰Session 适合服务端可控、快速吊销的场景Token 适合分布式、跨端、无状态 API的场景。但无状态也意味着难以主动作废等写完才发现要踢人、改密码、封号时Token 就是个烫手山芋。3.2 JWT 的组成和校验原理现在绝大部分 Token 都是 JWT全称是 JSON Web Token。它由三段 Base64Url 编码的字符串用点号拼接而成header.payload.signature。header 部分说明签名算法比如{alg:HS256,typ:JWT}payload 部分是业务声明常见的有sub主体通常放用户 ID、exp过期时间、iat签发时间、iss签发者、aud受众。注意 payload 只是 Base64 编码不是加密里面不要放密码、手机号这类敏感信息任何人解码都能看到。signature 部分如果用的是对称算法 HS256就是拿服务器密钥把header.payload做 HMAC 签名如果用的是 RS256就是用私钥签名、公钥验签。校验的标准流程是拆出三段、验签名、查exp、再根据业务需求查iss和aud。伪代码const decoded jwt.verify(token, publicKey, { algorithms: [RS256], issuer: auth.example.com, audience: web }); // 拿到 decoded.sub 后去数据库查用户 const user await findUserById(decoded.sub);这里要特别强调两点。第一algorithms必须固定不能直接拿alg字段决定算法否则可能存在 JWT 算法混淆攻击攻击者把RS256改成none或者HS256来尝试伪造。第二sub只是用户 ID业务上还得查一次用户表因为改头像、封号这些变化不会实时反映到 Token 里。顺带一提你在 AI 接口里看到的 token 是按文本段落拆出的计费单位和这里的登录 Token 只是同名。我之前见过有人把token 收费标准和JWT 续签混在一起搜这俩完全是两个世界的概念别被名字误导。3.3 Token 失效与续签的几种实用做法Token 无状态带来的最大痛点就是怎么续签和怎么让旧 Token 失效。比较常见的做法是双 Token 方案一个 Access Token 短期有效用于真正访问接口比如 15 分钟一个 Refresh Token 长期有效比如 7 天或 30 天只负责去换新的 Access Token。客户端在快过期时调刷新接口后端拿着 Refresh Token 验证后生成新的 Access Token。如果 Refresh Token 也过期了用户就得重新登录。用 JWT 做 Refresh Token 续签时我强烈建议做一次一换也就是每个 refresh_token 只能用一次用完之后立刻作废并生成新的 refresh_token。这样即使旧的 refresh_token 泄露攻击者也用不了第二次。实现上可以通过 Redis 存 refresh_token 的 jtiJWT ID每次刷新时校验 jti 是否存在存在才允许换新后立刻删掉旧 jti、写入新 jti。如果攻击者试图重放旧的 refresh_token会因为 jti 已经不存在而直接被拒。还有一种常见需求是保持登录状态的滑动过期也就是用户在操作时自动续期。会话方案可以直接重置 TTLJWT 方案没法直接改动已签发 Token更简单的做法是让前端在 Access Token 剩余时间低于某个阈值时自动调刷新接口拿到一个新 Access Token 再重放原请求。实测下来这种快到期才刷新的策略比固定时间刷新要稳能减少不必要的刷新请求也降低并发刷新导致 token 互踢的概率。4. 三套方案怎么选场景化对比与组合策略4.1 一张表看懂区别维度CookieSessionToken状态存放位置浏览器端服务器端客户端服务端存储不需要需要内存/Redis/DB不需要身份信息暴露风险直接可见客户端只看到随机 ID可解码不能放敏感字段失效控制靠过期时间可主动删记录需黑名单或短过期跨域/跨端受 SameSite/Domain 限制依赖 Cookie更适合 App/多端典型场景登录态载体、埋点传统 Web 应用开放平台、API 网关但注意这张表不代表三者必须对立。Cookie 经常和 Session 一起出现因为 Session ID 要通过 Cookie 传递Token 也可以放在 Cookie 里借助 HttpOnly 降低 XSS 风险CSRF 防护里的csrf_token、OAuth 2.0 的 code、access_token 也都会跟 Cookie/Token 混在一起。4.2 我这几年常用的组合策略如果是传统多页面 Web 应用我用 Cookie Session 通常就够了。用户信息放 RedisSession ID 放 HttpOnly Cookie登录时强制重新生成 Session ID 防止 Session 固定攻击。优点是可以随时把 Redis 里的 Session 删掉实现删除设备、强制下线这类功能。如果是前后端分离的 SPA或者有 App、小程序、第三方开发者接入的开放平台我更倾向于 Opaque Token 或 JWT。Access Token 短时效Refresh Token 长时效并且封装一个统一的鉴权中间件所有接口都从 Authorization 头里取 Token校验通过后再把用户信息放进请求上下文。还有一种常见方案是混合态登录后先发 Refresh Token把它放进 HttpOnly Cookie前端无法用 JS 读取同时把短效的 Access Token 放在前端内存或安全存储里。刷新接口通过 Cookie 自动携带 Refresh Token 来换新这样既解决了跨端手动携带的问题又降低了 Token 被 XSS 偷走的概率。选择的关键不是比谁更高级而是比谁更适合你的部署环境和团队运维能力。没有 Redis 的小项目硬上 Session 集群体验很差没有统一密钥管理体系的项目硬上 JWT一旦密钥泄露整个系统都要重发。5. 实操过程登录、调试、排查一条龙5.1 一个能直接抄的登录流程我以 Node.js 后端为例登录接口收到正确的用户名密码后应该做以下几件事开启 Session、写入用户标识、重新生成 Session ID、设置安全 Cookie。用 express-session 时的核心逻辑router.post(/login, async (req, res) { const { username, password } req.body; const user await verifyUser(username, password); if (!user) return res.status(401).json({ error: bad credentials }); // 登录成功后必须重新生成 session防止 session fixation req.session.regenerate((err) { req.session.userId user.id; req.session.role user.role; req.session.save((err) { res.json({ code: 0 }); }); }); });如果用的是 Token 方案登录成功后的核心逻辑就是生成 Access Token 和 Refresh Token并把 Refresh Token 存进 Redis再返回给前端const accessToken jwt.sign({ sub: user.id, role: user.role }, secret, { expiresIn: 15m, issuer: auth.example.com }); const refreshToken jwt.sign({ jti: randomUUID() }, refreshSecret, { expiresIn: 7d, issuer: auth.example.com }); await redisClient.set(refresh:${refreshToken.jti}, user.id, { EX: 7 * 86400 });前端请求受保护接口时统一在请求拦截器里加请求头Authorization: Bearer ${accessToken}5.2 用抓包工具串起 cookie 和 token 的调试很多人调试接口时最烦的是登录成功了下一个接口却提示未授权。原因基本都在 Header 没带对。做本地调试时我常用 Reqable 或浏览器开发者工具来检查整个链路。在 Reqable 里登录接口返回的Set-Cookie会被自动记录下来它有一个自动 Cookie 管理的能力如果你开启了自动携带 Cookie同一个域名的后续请求会自动把响应里的 Cookie 代入下一个接口不需要你手动复制粘贴。这个功能在看第三方登录、扫码登录时尤其好用因为中间可能经历多次 302 跳转每次跳转都会带上不同的 Set-Cookie。如果是 Token 接口通常没有自动携带这回事。你必须把登录接口返回的access_token取出来手动或者用流程变量赋值到后续接口的 Authorization 头。小程序、手机 App 里看不到 Cookie 面板一般也是在抓包软件里看登录请求的响应体把 token 提出来再排查后续请求。5.3 常见报错排查速查表我把这几年在社区里被问得最多的报错整理成一张速查表以下都是我实际排查过的方向报错关键词真实含义优先排查There is no session with id服务端找不到 Session 记录Redis key 是否存在、Cookie 里的值是否被重置、服务端是否重启Could not open Hibernate session for transaction数据库会话获取失败数据库连接池、事务配置不是 Web 登录 Sessionsession file locked (timeout 60000ms)Session 文件锁等待超时并发请求、慢接口建议换 Redistoken expiredAccess Token 过期走刷新流程不要直接让用户重新登录Your access token could not be refreshedRefresh Token 过期或已被换掉检查 refresh_token 是否轮换、Redis 里的 jti 是否还在token exchange failed: token endpoint returned 403OAuth 授权链路失败client_id/secret、redirect_uri、code 是否过期业务范围限制sign-in could not be completed token exchange failed...第三方登录交换 Token 失败服务端时钟偏差、签名算法、请求来源 IP 是否在白名单内OAuth 的 token exchange 报错尤其需要看完整链路。常见原因是 authorization code 只能使用一次但你重放了它或者 redirect_uri 和申请应用时填的不一致也有服务端时钟偏差导致签名校验不过的情况。遇到token endpoint returned 403时先看服务器的时间和 NTP 对时再去看授权服务器的日志通常日志里会写清楚具体是哪一项校验失败。5.4 一套我每次都要过一遍的安全清单最后分享一个我上线前必查的清单不一定覆盖所有场景但能挡住绝大多数低级事故。第一所有认证类 Cookie 必须加HttpOnly身份数据能不放在 JS 可读范围就不放。第二生产环境必须有Secure属性测试环境要通过 HTTPS 访问再验证。第三SameSite 不要随手写None只有明确需要跨站携带时才用且必须加Secure。第四Session 方案登录成功后必须regenerate删除旧的 Session ID避免 Session 固定攻击。第五不要在 Cookie 里直接放roleadmin、user_id123这种可读明文一定要用服务端状态或者签名值。第六Token 的签名密钥要和业务配置分离开定期轮换且不要把私钥放进前端代码或版本库。第七无论 cookie 还是 token都在后端统一做过期校验不能只靠前端隐藏按钮。这套组合我在实际项目里反复打磨了很多年cookie 负责承载、session 负责可控、token 负责跨端真正踩过的坑大多不在概念本身而在通行凭证的失效时机和隔离边界上。做登录鉴权心里始终要有一条主线你到底把信任放在哪里客户端、服务端存储还是签名本身想清楚这个问题再回去看各种报错和配置项都会顺畅很多。
返回列表