Cookie与Token应用场景对比:跨域、移动端为什么不能只用Cookie?
经常有朋友拿着报错记录来问我明明登录的时候一切正常怎么就突然给我弹出来token exchange failed或者隔了一晚再打开系统token失效要重新登录另一边也有人把浏览器的 Cookie 导出来导去问我能不能把整个系统改成 Cookie 登录省得天天跟刷新令牌较劲。说实话Cookie 和 Token 这两个词几乎每天都会碰到但很多人对它们的理解还是停在一个模糊的层面上——知道都是“保存登录状态用的”但到底什么时候用哪个、为什么有的场景用 Cookie 就是不行说不出个所以然。我们把话说透。这篇文章就是围绕“Cookie 和 Token 的应用场景优势比较以及 Cookie 不能使用的场景补充”来写的。我会用实际项目里的经验把两者的本质区别、适用边界、切换方案还有token失效、jwt实现token续签、跨域掉登录这些高频问题一次性讲清楚。如果你是做 Web 开发、接口联调或者正在设计一个需要多端登录的系统这篇文章应该能省下你不少瞎折腾的时间。1. 先摸清两个主角Cookie 和 Token 到底是啥1.1 Cookie 在浏览器里的真实身份很多人问“cookie中文”度娘会告诉你是“小饼干”但在我们开发里别这么叫。Cookie 本质上是一小段文本由服务端通过Set-Cookie响应头生成然后浏览器把它以键值对的形式存下来。下次访问时浏览器会自动把这个文本塞进请求头里服务端读出来就知道“你是谁”。这里回答一个高频问题Cookie 是在请求头里吗是的绝大多数情况下你会在 HTTP 请求里看到Cookie: session_idabc123这样一个请求头。但要注意请求头里的 Cookie 是“携带”出去的身份凭证而服务端下发凭证动作是通过Set-Cookie响应头做的。这两个头不要搞混排查问题的时候特别关键。Cookie 的归属感很强它属于某个域名。浏览器严格遵循同源策略A 网站存下的 Cookie你在打开 B 网站时不会主动带过去。这也是为什么正常业务里Cookie 能天然地实现“用户只对自己登录过的网站有效”。不过保护太好也会带来麻烦后面讲跨域时你会看到它有多让人抓狂。1.2 Token 是一个更“独立”的凭证Token 就更像一个临时身份证。它不依赖浏览器自动携带而是由你的业务代码把一串凭证放到请求里最常见的做法是放到Authorization: Bearer token这个请求头里。也有人说“token在请求体里行不行”技术上可以但不符合惯例也不安全。Token 通常分为两种一种是很随机的“opaque token”就是单纯一串不透明的字符服务端拿到后再去查表另一种是 JWTJSON Web Token它把用户信息、过期时间等打包成一个带签名的字符串服务端验签即可。JWT 是“自包含”的所以它在分布式系统里很受欢迎。从身份保存的角度看Token 和 Cookie 其实不是同一层的东西。Token 是一个“凭证”Cookie 是一种“存储和传递机制”。你用 Cookie 可以存 Token你没有 Cookie 也照样可以手动把 Token 放请求头里。很多文章把两者对立起来是因为它们背后代表了两种经典的会话管理模式服务端会话管理 和 客户端无状态认证。1.3 Cookie、Session、Token 三者的正确关系我发现很多人把 Session 和 Cookie 混着说。Session 是服务端的内存或存储空间里保留的一份会话数据Session ID 通过 Cookie 发给浏览器保存。所以“Cookie 失效”不等于“Session 没有了”“Session 过期”也不等于“浏览器里的 Cookie 被清除”但实际体验通常都被归结为“掉登录”。Token 模式则可以完全不要 Session。服务端不保存会话状态只要验签通过就相信请求。这就是无状态认证的基础。JWT 实现 token 登录验证时只需要在生成 token 时把exp过期时间设好之后每次请求拿着 token 过来服务端解出签名和时间戳就行不需要担心在哪个服务器上存了 session。一句话总结Cookie 解决“怎么把身份凭证存下来自动带过去”的问题Token 解决“身份凭证长什么样、怎么验证”的问题。两者可以组合可以独立关键看你所在的应用场景。2. 应用场景优势比较什么时候该选 Cookie什么时候该选 Token2.1 传统网站形态下Cookie 依然是最顺手的选择如果你的产品是服务端渲染的传统网站比如用 PHP、Java 写的页面用户在登录后服务端要维持他的浏览状态那 Cookie 方案往往是最省事的。浏览器自动在请求头带上 Cookie你不需要在前端为每个请求手动拼接凭证用户刷新页面、跳转页面身份信息都不会丢。而且服务端可以很方便地对 Cookie 加限制HttpOnly属性让 JavaScript 读不到它Secure属性让它在 HTTPS 下才发送SameSite控制跨站携带策略。这些选项合在一起可以在很大程度上对抗窃取和 CSRF 攻击。对传统表单登录、后台管理系统这类场景Cookie 的成熟度和浏览器兼容性无可挑剔。很多人误以为“Cookie 会让网站访问速度加快”这个说法我不太赞同。实际上Cookie 是附加在每个请求头里的数据服务端还要解析它。如果 Cookie 太大超过 4KB 很正常反而会增加请求体积拖慢网络传输。真正加快访问速度的是缓存、CDN 这些机制不是登录凭证。2.2 在 API 和多端应用面前Token 优势明显一旦你的产品要从浏览器扩展到 App、小程序、第三方开放平台Cookie 就会很吃力。因为原生 App 没有浏览器那种自动管理 Cookie 的机制小程序对 Cookie 的支持也时好时坏。你不可能要求 App 开发者去手动维护一个 Cookie 仓库。Token 则可以很干净地由客户端存储并在每个请求的请求头里显式携带跨端共用同一套认证逻辑。无状态是 Token 的另一张王牌。如果服务端扩展出十台机器Session-Cookie 模式需要做会话同步或者粘性负载均衡否则用户第一次请求打到机器 A第二次打到机器 B 就可能找不到 Session。使用 JWT 之后每台机器只验签就完事不需要共享存储水平扩展的阻力会小很多。权限控制上 Token 也更灵活。你可以签发一个只包含“读权限”的 token另一个包含“写权限”的 token甚至限定某个 token 只能调用某个接口。这在微服务场景里非常灵活。Cookie 虽然也能存权限信息但它天生就是“让服务器把我认出来”的机制不太适合做细粒度授权。2.3 一张表看懂核心差异对比维度Cookie Session 模式Token 无状态模式凭证存储位置浏览器 Cookie服务端存 Session 或存储数据客户端自持服务端验签或查证携带方式浏览器自动加Cookie请求头代码显式加Authorization: Bearer同源依赖强受同源策略限制弱可以跨域调用服务端状态有状态需要会话同步或粘滞无状态适合水平扩展移动端适配原生 App 处理麻烦天然适合移动端安全主风险CSRF、XSS 窃取 CookieToken 泄露、XSS 窃取存储令牌过期与刷新由服务端 Session 过期控制access_token refresh_token 双模式典型场景传统同源网站、后台管理系统API 服务、移动 App、单点登录表格列出来就很直观Cookie 适合“浏览器同源服务端渲染”的经典场景Token 适合“多端跨域大规模服务”的现代分布式场景。没有绝对的谁好谁坏只有合不合适。3. Cookie 不能使用的场景补充踩过的坑都在这3.1 跨域调用接口时Cookie 的“自动携带”反而成了障碍我接手过一个前后端分离项目前端域名app.example.com后端接口域名api.example.com虽然二级域名不同也属于跨域。前端用 fetch 调接口时默认不会带上 Cookie必须额外配credentials: include后端也要在响应头里允许跨域并带Access-Control-Allow-Credentials: true还要明确设置Access-Control-Allow-Origin为具体域名不能用*。这套配置听起来不难但一旦中间夹了 CDN、WAF 或 API 网关事情就复杂了。网关有时候会改写域名请求头里的 Cookie 被剥离或篡改你排查半天发现是中间某个环节把 Cookie 丢了。这种场景下Token 反而更稳因为它是放在请求头里的独立凭证跨域也好、经过网关也好只要透传Authorization头就行。还有更麻烦的如果第三方平台服务你的页面你想让用户带上自己的登录状态Cookie 的第三方限制会让你绝望。浏览器更新得越来越激进Safari 默认拦截第三方 CookieChrome 也在逐渐掐掉。你辛辛苦苦在 iframe 里做的嵌入登录可能第二天就被浏览器策略“升级”掉了。3.2 移动端、桌面端和小程序场景Cookie 基本靠不住原生 App 不发Set-Cookie也照样活但浏览器那套 Cookie 管理机制在原生环境里并不存在。HTTP 客户端库比如 OkHttp虽然可以像浏览器一样帮你管理 Cookie但你还得配置一个CookieJar。逻辑要自己写过期要自己清不同的网络库还各有各的脾气。正因如此移动端做登录认证业界默认首选 Token。小程序场景更有意思。小程序WebView 经常和原生层混着走Cookie 在 WebView 和原生页面之间的同步很容易出问题。你可能遇见这种情况在 WebView 里登录成功跳回原生页面后又提示未登录因为原生请求没有带 WebView 种下的 Cookie。换成 Token 后把令牌存在全局变量或安全存储里每个请求显式带过去就完全规避了这一类“状态不同步”。3.3 分布式、微服务和云函数环境下Session-Cookie 模式可能反噬架构Cookie 模式的核心是“服务端有 Session”。那么问题来了服务端扩容到多台机器Session 放哪儿要么引入 Redis 集中存储要么让负载均衡器做粘性会话保证同一个用户的请求总是落到同一台机器。前者增加一层基础设施后者在重启、缩容时会导致登录态闪断。Token 无状态验证就没有这个烦恼任何一台机器验签后都能独立处理请求。云函数和 Serverless 场景更是如此。函数实例是无状态的甚至每次调用可能由不同实例执行。你用 Cookie 存一个 Session ID结果后续请求打到另一个函数实例Session 数据根本不在那里。JWT 因为自包含在 Serverless 里表现极好只要服务端能验签就行。这只是架构层面的理由。还有一个经常被忽略的细节Cookie 限制大小标准限制 4KBToken 尤其是 JWT 本身已经不算小再加上一堆业务字段可能超过 4KB。要往 Cookie 里塞 JWT你会立刻撞上体积天花板。与其费劲压缩 token、把字段删来删去不如直接把 Token 放在 Authorization 头里大小限制宽松得多。3.4 安全敏感场景下Cookie 的暴露面比想象中大Cookie 由浏览器自动携带看着方便也意味着攻击者有时不需要费劲读 Cookie只要诱导浏览器主动把 Cookie 发出去就行。经典的 CSRF 攻击就是这么干的你在银行网站登录了再去点恶意链接恶意脚本发一个跨站请求Cookie 自动被带上服务端以为是你操作。虽然SameSite和 CSRF Token 能防御但很多人并不了解或者为了保护体验故意放松了SameSite。Token 同样有风险主要是 XSS 导致令牌被窃取。很多前端项目喜欢把 Token 放在 localStorage结果一个不小心被注入脚本令牌连锅端。但 Token 的防御手段比 Cookie 多你可以做短生命周期 access_token配合 refresh_token你可以把 refresh_token 放在HttpOnlyCookie 里实现安全性和刷新体验的平衡你可以为 Token 绑定 IP、设备信息发现异常直接吊销。Cookie 模式一旦 Session 被劫持常规处理只能靠 IP 和用户日志去排查非常被动。3.5 当你需要“临时授权”和“可撤销力度”的时候Cookie 明显不够灵活想象这样一个场景你做了一个开放平台用户授权给第三方应用查看自己的部分数据。Token 模式下你可以签一个权限受限的 token比如只允许读取用户头像和昵称有效期七天。到期前用户可以在平台上查看“已授权的应用”一键撤销之后所有拿着这个 token 的请求立刻失效。Cookie 模式做这件事不是不行但得先分清域第三方应用难道也用你的域名不可能。你只能给第三方应用一个独立的凭证那东西本质上还是 Token。所以涉及到跨系统的授权Cookie 的“域归属”特性反而成了限制。你可以说“Cookie 不能使用的场景”核心就是这些跨域、移动端、无状态扩展、细粒度授权、临时授权撤销。4. 实操怎么判断用哪种方案怎么落地4.1 选型决策照着这三个问题问自己选 Cookie 还是 Token不需要过度抽象就问自己三个问题。第一你的用户端是浏览器为主还是 App、小程序、第三方接口都有只有浏览器Cookie 最省心有移动端Token 是必然选择。第二你的服务端是单个单体应用还是分布式、微服务单体同源Cookie 的 Session 管理非常成熟分布式、跨团队Token 无状态验证更省事。第三你的业务是否允许“用户长期自动登录”如果你的业务是后台管理希望用户关闭浏览器后 session 也能很快过期Cookie 的会话过期机制很好做。但如果你做的是 ToB 企业集成服务用户希望一次对接长期有效Token 的 refresh_token 模式明显更友好。我自己的经验是新项目一律先考虑 Token 方案除非产品形态非常传统的服务端渲染网站。因为 Token 方案推倒重来的成本更低而 Cookie 方案一旦覆盖到移动端和跨域后期改造很痛苦。4.2 Token 落地时的核心参数设计先用 JWT 实现 token 登录验证时有两个时间点必须考虑清楚access_token 的过期时间和 refresh_token 的过期时间。结合实际项目经验我给一个相对稳的配法access_token15 分钟到 2 小时。太短会让用户频繁重新登录太长则泄露风险高。内部系统可以设长一点面向公众的接口建议短一点。refresh_token7 天到 30 天。它是一种“更长时间有效但只用于换取新 access_token”的凭证。拿到你的 refresh_token攻击者不能直接调业务接口只能在刷新 token 上做文章所以它一般会存储得更安全还要绑定客户端 ID、设备信息。做成流程就是刷新接口POST /auth/refresh接收 refresh_token校验通过后返回新的 access_token 和新的 refresh_token。刷新后的 refresh_token 会轮换旧的一律作废。这样就算某一个 refresh_token 泄露攻击者也只能用一次且用户登录后旧 token 立刻失效。这个模式习惯上叫jwt实现token续签核心是“刷新令牌轮换”。还有一个小技巧refresh_token 不一定要做成 JWT。你可以把它设计成不透明字符串存到服务端数据库里使用一次删除一次。这样虽然多了一次查库但规避了 JWT 一旦签名泄露就难以失效的问题。安全性要求高的系统我更推荐这个方案尤其是当你有刷新令牌绞杀refresh token reuse detection需求时。4.3 从请求头到浏览器存储Cookie 和 Token 的实际差异对比代码最直观。同一个登录请求Cookie 模式下服务端返回Set-Cookie头HTTP/1.1 200 OK Set-Cookie: session_idabc123; Path/; HttpOnly; Secure; SameSiteLax之后浏览器发起接口请求时自动带上GET /api/profile HTTP/1.1 Cookie: session_idabc123Token 模式下客户端拿到令牌后把它保存在内存、localStorage 或安全存储里每次请求手动加上GET /api/profile HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...注意一个细节Token 模式下浏览器不会帮你做任何事。前端代码必须在每个请求拦截器里带上Authorization请求头。很多人刚开始切 Token 时不适应特别容易忘记给某些请求加认证头结果就是偶尔 401检查半天才发现漏了。关于存储位置我强烈建议别把 access_token 放在 localStorage防止 XSS 一锅端。实际项目中常用的是access_token 放内存比如 Vue 的 store 或 JS 变量页面刷新时用 refresh_token 换新的refresh_token 放在HttpOnlyCookie 里。这样既解决刷新后丢失 access_token 的问题也让 JavaScript 无法读取 refresh_token安全性大幅提升。这个混合方案很多大厂都在用。4.4 Cookie 不能使用的场景补充之外什么时候强行用 Cookie 很坑有些场景Cookie 确实不能直接使用但你可以“借用”它。比如跨域调接口时Cookie 带不上可是 Cookie 本身又能帮你做 refresh_token 的静默续期。这时候你就需要把 refresh_token 种到一个“较温和”的域下同时做好 CORS 配置保证前端credentials: include能生效。这里要注意SameSite属性的选择。如果你的刷新接口和前端页面是同一个主域名下的不同子域SameSiteLax通常够用如果你要做嵌入 iframe 的静默刷新可能得SameSiteNone; Secure但浏览器对第三方 Cookie 的限制越来越多这条路越来越难走。我的建议是不要过度依赖 Cookie 做跨域静默续期可以考虑用“短连接中台刷新”这类替代架构别和浏览器策略硬刚。5. 常见问题与排查技巧实录5.1 token exchange failed、refresh_token 为空这类报错怎么拆解网络上经常能看到类似token exchange failed: token endpoint returned status 403 forbidden或failed to refresh token: 400 bad request: invalid refresh_token: empty string的报错。这类问题核心在“外联服务”的令牌交换环节未必是你代码写得不对。可以拆成三步排查看这一步到底在做什么操作。token exchange failed通常发生在授权码换访问令牌、或者用刷新令牌换新令牌的瞬间。你要先确认自己调用的 endpoint 是否正确请求参数是不是齐了。看错误里的 status code。403 很可能因为 IP 地域限制、账号权限不足400 基本是参数问题比如refresh_token传了个空字符串或者传错了明文令牌。看日志里的完整 URL 和响应体。很多框架只会显示错误概要实际原因藏在响应体的 error code 里。我做过一次类似联调对方平台返回country, region, or territory not supported。说难听点这个业务地域被限制在配置里了你往死里调代码也没用。正确做法是查平台的文档看支持了哪些地区、是否需要额外在白名单里加你的来源 IP。这类问题不是单纯的技术问题先业务后技术别一上来就改代码。5.2 浏览器开发者工具里看不到 Cookie 怎么办有段时间我调一个登录接口明明服务端响应头里有Set-Cookie但开发者工具 Application 面板里就是找不到。后来发现是两个原因一个是当时测试环境不是 HTTPS但 Cookie 标了Secure浏览器直接拒收另一个是 Chrome 版本对未指定SameSite的 Cookie 默认按Lax处理跨站环境把它拦了。排查方法很简单先看请求的Set-Cookie响应头逐项核对属性再看当前页面环境是不是localhost、有没有跨域最后看浏览器设置里是不是开启了“阻止第三方 Cookie”。很多人开的无痕窗口还有默认的跟踪保护也会把 Cookie 拦掉。建议用普通窗口开开发者工具 Network 面板看请求和响应原文比单看 Application 面板快得多。5.3 每次刷新页面都掉登录十有八九是 access_token 存储位置不对某天测试反馈我明明登录成功了一刷新页面就打回登录页。查代码发现前端把 access_token 放在 JavaScript 变量里没有持久化刷新后变量清空也没有实现 refresh_token 自动续期。场景很典型。解决方案分两种如果你不追求刷新静默可以直接把 access_token 存 sessionStorage刷新不丢但关闭标签后就没了。适合对安全要求高的内部系统。如果你想要“登录一次长时间不掉”就按前面说的 refresh_token 方案把“长期刷新能力”放在 HttpOnly Cookie 或安全存储里前端刷新后调用 refresh 接口换新 access_token。注意一个坑refresh 接口请求成功前前端可能有多个并发请求在跑全部带上过期的 access_token结果同时返回 401。这时候所有请求都去触发 refresh刷新令牌会被并发刷新多次最后只有最后一次生效其他全失败。解决办法是在前端做一个“单飞”逻辑统一用同一个 Promise 去刷新其他等待中的请求拿到新 token 后重放。5.4 Cookie 与 Token 安全配置避坑清单根据这几年摸爬滚打我整理了一个清单新手照着做能少踩很多坑Cookie 一定要加HttpOnly让 JS 读不到登录凭证HTTPS 环境必须加Secure否则凭证会被明文传输SameSite属性必须显式设置不要赌浏览器默认值Token 不要放在 localStorage尤其是 access_tokenXSS 一来全完蛋access_token 过期时间短一点refresh_token 过期时间长一点但要轮换刷新令牌被重复使用时要全部强制下线不能只发新令牌所有涉及刷新令牌的接口必须校验客户端 ID、设备信息不能单独以令牌为准不要在打印日志时输出 token 和 Cookie 原文泄露可能就是从日志开始的服务端收到 Token 后至少校验 signature、exp、iss、aud 这些标准声明跨域使用 Cookie 时CORS 配置要具体域名不可以用*通配。这些点很多官方文档也写但执行起来容易遗漏。我特别强调第一和第三因为顺手加两个属性就能挡掉绝大多数低级攻击。5.5 一个实际问题Cookie 里的 JWT 到底行不行有人说既然 JWT 是 Token那你把 JWT 放在 Cookie 里是不是就二者得兼了是可以但不要把两者混成一团。你在 Cookie 里放 JWT仍然要面对 Cookie 同源策略、CSRF 风险、体积限制。解决思路是Cookie 存一个随机会话 ID服务端将“会话 ID 对应组 Token”缓存在 Redis 里而不是直接把 JWT 塞进 Cookie。这样既利用 Cookie 自动携带的便利又能随时吊销服务端的会话状态还避开了 JWT 的“无法主动失效”问题。反过来前端拿到 JWT 后想加一层保险也可以用两步走access_token 放在内存refresh_token 放在 HttpOnly Cookie。前端请求时手动加 Authorization 头刷新时 Cookie 自动带 refresh_token。这是我个人最喜欢的折中方案兼顾安全、刷新体验和跨域可能性。6. 最后再补两句实操体会几年前有个项目最初信了“Cookie 已死Token 至上”的说法把整个后台管理系统的会话全换成无状态 Token。结果后台用户经常抱怨为什么我换了电脑就要重新登录为什么接口频繁的 401后来我把后台管理系统改回 Session-Cookie只把对外开放的 API 保留 Token 认证问题立刻消停了。另一个项目则正好相反早期用 Session-Cookie 做了移动端 API结果每次版本发布都要处理会话丢失的问题痛不欲生。结合这些经历我的结论是Cookie 和 Token 不是新旧代替的关系而是不同场景下的两种工程选择。浏览器端同源会话Cookie 依然顺手跨端、跨域、高扩展和临时授权Token 更有优势。Cookie 不能使用的场景往往是跨域、移动端、分布式和无状态要求的混合体与其硬着头皮改造 Cookie不如早点把认证层切换成 Token 方案。如果你现在正在为选哪种方案纠结我的建议是先明确自己的用户端有哪些、服务端会不会快速扩容、业务上是否需要临时撤销授权把这三个答案写下来再回头看这篇文章的对比表方向基本就定了。希望这些踩坑记录能让你少走几个来回。