
1. 从“我是谁”说起HTTP无状态下的身份之问我当年第一次被问到“Session、Cookie、Token、JWT有什么区别”时是在一次项目评审会上。当时我用的方案是“Session Cookie”另一个同事坚持用“JWT”两个人争了半天谁也说服不了谁。后来我们发现争论的根源根本不是谁的技术更先进而是两个人对“登录状态到底应该存在哪里”这件事的理解完全不一样。这个问题的起点其实是一个几乎所有后端开发者第一天就会遇到的困惑HTTP协议本身不记住任何人。你向服务器发了两次请求服务器眼里这两次请求没有任何关系。它不知道第一次请求的是你第二次请求的还是你——除非你每一次都主动告诉它“我是谁”。这就是网络世界里最经典的“无状态stateless”问题。你登录了购物网站加入购物车然后刷新页面如果服务器不记得你购物车就空了一切回到原点。可现实是你刷新之后购物车还在这说明一定有什么机制在帮服务器“认人”。Session、Cookie、Token、JWT就是围绕“怎么认人”这个问题衍生出来的四套主流方案。这四种东西听起来像同一个问题的四种解法但实际上它们的定位差别非常大有的是一张“通行证”有的是一本“档案簿”有的是一枚“刻章”有的是一张“身份证”。而且现实中你几乎不可能只用其中一种大多数系统都是它们组合在一起工作。本文不打算照本宣科背概念而是从我实际开发和排查问题的经验出发把这四个东西的原理、各自的边界、组合方式、常见坑和安全漏洞讲清楚。适合刚入门的后端和前端同学也适合做了几年项目但对这几个概念一直“用了但没想透”的开发者。先说结论方便你有全局感Cookie是浏览器本地存放数据的载体Session是一份存在服务器上的会话记录Token是一串由服务器签发的凭据客户端拿着它证明身份JWT是Token的一种特殊格式自带信息且经过签名。接下来我一个个拆开讲讲完你再看它们之间的区别会觉得非常通透。2. Cookie浏览器里的持久化便签以及它的边界Cookie是我建议你第一个搞懂的东西因为它是所有会话方案的最底层基础设施。它不是什么高深的技术说白了就是一小段文本由服务器通过Set-Cookie响应头写到浏览器里浏览器会忠实保存并在后续请求中通过Cookie请求头带回去。注意决定“带不带”和“带什么”的不是业务代码而是浏览器内核。你要记清楚这一点很多奇奇怪怪的登录问题根源都在这里。2.1 Cookie如何随请求偷偷“自报家门”想象你在银行办业务每次到柜台掏出身份证柜员看完之后在系统里记一笔。Cookie就类似那个“随身的身份证复印件”只不过它是由办事机构发给你、你随身带着、下次再来时主动递上去的。HTTP请求里Cookie的行为一模一样你第一次访问服务器服务器在响应头里写Set-Cookie: session_idabc123; Path/浏览器收到后存起来之后你每次访问该域名下的路径请求头里都会自动带上Cookie: session_idabc123。这里有一个经常让新手误解的点Cookie不是在请求体里而是在请求头里。我看过有人写代码在request.body里找Cookie找了半天找不到这就是对位置不清楚。浏览器自动帮你拼请求头业务代码通常不用管但如果你用axios、curl这类工具手动发请求就必须自己在Cookie请求头里写值。另外Cookie的“归属范围”由Domain和Path两个属性控制。Domain决定哪些域名接受这个CookiePath决定哪些路径下会发送。一个只允许/user路径携带的Cookie你访问/order时浏览器是不会带过去的。2.2 属性背后的安全博弈HttpOnly、SameSite、Secure很多年以前网站普遍把用户登录状态直接放在Cookie里前端JS也能读到结果被XSS跨站脚本攻击一打用户的Cookie直接被偷走账号被盗事件层出不穷。后来浏览器和服务器开始给Cookie加各种约束属性相当于给这张便签贴上了使用说明和安全封条。先说HttpOnly。这个属性的作用很简单粗暴带了这个标记的Cookie前端JavaScript代码里用document.cookie根本读不到只有浏览器自动发送时才会带上。这样就算页面被注入了一段恶意脚本它也摸不到你的登录态Cookie。我写登录相关的代码时凡是用Cookie存会话标识几乎无脑加HttpOnly。再说SameSite。这个属性是近几年CSRF跨站请求伪造防护的关键。Cookie默认会跟着请求发送而CSRF攻击恰恰是利用了这一点你在A网站已登录攻击者诱导你的浏览器去请求B网站的某个敏感接口因为Cookie会自动携带服务器就以为是你在操作。SameSiteLax模式下跨站发起的GET请求会带Cookie但POST这类危险请求会禁止携带SameSiteStrict则更严格跨站一律不带SameSiteNone则必须配合Secure才能使用它允许跨站携带但要求连接必须走HTTPS。这里有个经验如果你做的是第三方嵌入类的服务需要别的网站通过iframe加载你的页面并保持登录SameSite设置不恰当会出现“Cookie不生效”的诡异问题调试起来非常折腾。Secure属性则要求只能通过HTTPS发送CookieHTTP明文连接下浏览器会拒绝携带。别小看这个属性明文传输的Cookie等于把会话标识直接写在网络上抓包的人一抓一个准。2.3 中文乱码与“Cookie在请求头里吗”这类实操疑问我在处理线上问题时遇到过几类高频Cookie问题这里集中说下都是可以对照排查的。第一类中文乱码。Cookie规范里允许的值本身是ASCII范围你要直接往Cookie里写中文轻则乱码重则被浏览器直接丢弃。正确做法是写之前用encodeURIComponent编码读取时再解码。我看到很多项目在Cookie里放用户昵称、城市名不做编码处理换个浏览器行为都不一样这就是典型的“本地能跑线上必炸”。第二类Cookie丢失或带不过去。排查思路按顺序来先看响应头里有没有Set-Cookie没有则问题在服务端有则检查Domain和Path对不对比如你设置了.example.com但访问的是www.example.com按Cookie规则是能带上的但如果你设置了example.com而不带点部分浏览器对子域的携带就有差异再看SameSite属性因为现在浏览器默认值已经是Lax了很多老的内部系统没适配跨站跳转时Cookie就不见了最后看Secure非HTTPS下设置了这个属性的Cookie存不下来。第三类浏览器限制。Cookie不是无限量的单个域名下的Cookie总数量有上限不同浏览器约20到50个单个Cookie大小通常限制在4KB左右。超出后浏览器会静默丢弃不报错、不提示。所以Cookie适合存“标识符”而不是“大对象”。如果你想把用户购物车整体塞进Cookie里且不说安全性4KB很快就爆了。3. Session服务端内存中的会话账本Cookie解决了“客户端留痕”的问题但它有个致命弱点存下来的东西是明文可见的用户完全可以篡改。假设服务器用Cookie存isLoggedIntrue用户自己把Cookie改成false——当然这不影响登录但如果存的是角色字段比如roleadmin用户在浏览器控制台里一改就能把自己变成管理员。这种方案没人敢用所以业界很快演化出Session身份数据不放在客户端而是放在服务端客户端只保存一个无法预测的“编号”也就是session id。3.1 会话标识的生成、比对与销毁Session的核心流程可以概括为三步签发、核对、销毁。用户登录成功时服务器生成一个足够随机的字符串session id把用户ID、角色、过期时间等数据存到服务器内存或Redis里然后通过Set-Cookie把这个session id下发给浏览器。之后每次请求浏览器自动带上Cookie服务器按session id去存储里找对应的数据找到就当这个用户是“已登录的某某某”。用户退出或超时时服务器主动把这份数据删除或标记过期session id瞬间变成废纸。这样做的好处是用户看到的只是“一串看不懂的随机字符”它本身不包含任何业务信息改也改不出什么名堂因为服务器只会把session id当作索引真正的数据存在你控制得住的那一侧。这也是Session和JWT最根本的分水岭Session是服务端状态JWT是客户端状态。下面我还会反复强调这一点。生成session id时随机性非常关键。我曾经接手过一个项目session id是用时间戳加用户ID拼出来的看起来挺复杂实际上攻击者完全可以通过构造猜测出别人的session id然后直接冒充对方登录状态。这就是典型的会话固定/会话预测攻击。正确做法是用足够强的随机源比如Python的secrets.token_hex(32)或Java里SecureRandom生成的64位以上随机串千万别用Math.random()或时间戳自己拼。3.2 分布式架构下的Session困境与替代思路Session这套模式在小单体应用里非常舒服但你一拆微服务、一上多实例问题立刻冒头用户第一次请求打到了A机器session存在A内存里第二次请求负载均衡把它转发到B机器B机器内存里根本找不到这个session用户就被判定为未登录。这就是著名的“分布式Session共享”问题。传统解法有几种。第一种是粘性会话sticky session让负载均衡器把同一个用户的请求固定打到同一台机器上。这办法配置简单但搞挂了那台机器所有在线用户全部掉线扩展性也差。第二种是Session集中存储把session数据抽出来放到Redis这类中间件里所有应用服务器共享访问。这也是目前主流做法缺点是要额外维护一套存储设施但换来了水平扩容的自由。第三种是Session数据里只放会话键实际业务数据从数据库实时拉取减少Redis压力。我个人更推荐一个思路能不存Session就不存Session。这就要引出下一节的Token方案了。当然不是说Token能完全取代Session有些场景比如强制单端登录、后台管理系统的在线用户管控Session仍然更合适后面选型部分我会细说。3.3 Session超时、会话固定与常见隐患Session有生命周期必须设置超时时间。很多系统默认30分钟这个值不是拍脑袋定的它直接影响用户体验和安全太短用户操作一半就被踢下线太长又增加了会话被盗后的风险窗口。医疗、政务这类敏感系统我一般设15到20分钟内部低敏感工具可以设半天。还有一个容易被忽略的细节活跃度计算。理想的设计应该做“滑动过期”也就是用户在持续操作时不断刷新过期时间而不是从登录那一刻起固定计时。固定计时的典型坑是用户看了个长视频或者写了篇长文档超出30分钟提交时报错让重新登录体验非常恶劣。会话固定攻击也是老生常谈但依旧常见的漏洞。攻击者先自己登录拿到一个合法session id塞给受害者用受害者登录成功后如果服务器没有重新生成session id攻击者就能用同一个id接管受害者的会话。防御手段就是在登录成功时无条件重新生成session id并销毁旧id。这套逻辑写死在每个登录接口里不要在代码审查时省掉。4. Token把“身份凭证”交还客户端的无状态方案讲完了Session自然就能理解Token的由来。Session要求服务端记住每一份会话数据这在大型分布式系统里是个沉重的包袱每次请求都要查一次存储存储还要保证高可用。于是有人想能不能让服务器“不记”让客户端自己保存完整的身份信息每次请求带过来服务器验证一下真假就行Token就是这种思想的产物。4.1 不透明Token的双重检查逻辑Token是一个比较宽泛的概念最简单的形态可以是一串随机字符串但它和session id最大的区别在于Token里本身可以携带信息而不是只当作一个单纯的索引。我们常说的“不透明Token”实际上服务端还是会在数据库或Redis里存一份对应关系这和Session没本质区别真正无状态的是JWT这种“自包含Token”。所以聊Token时我心里默认把它分成两类不透明Token服务器存状态和自包含Token服务器不存状态。很多人弄混Session和Token概念上纠缠不清其实你只要记住Session是“服务端账本 客户端编号”Token是“客户端凭证 服务端验签”。同一个系统里完全可以用Session存登录态同时用Token做接口鉴权两者不矛盾。比如我做过一个开放平台用户登录走Session管理后台第三方应用调用OpenAPI则用Access Token它们服务于不同的调用方和场景。4.2 Token与Session的本质差异存储位置变化带来的连锁反应把身份数据从服务端挪到客户端表面上只是存储位置的改变实际上引发了一连串影响这里面有几条值得展开。第一服务端变成了“没有记忆”的验票员。每个请求进来只需要验证Token签名、校验有效期不需要再查一次Session存储接口响应更快也少了一个中间件依赖。代价是服务端无法主动让某个Token立即失效。在Session体系下你把session销毁用户下次访问立刻失效在JWT体系下你销毁了客户端手里那张票在过期之前仍然能通过验签。这就是“Token失效”问题最常见的根因也是网上检索量特别大的痛点。第二数据的隐私边界变了。Token里如果带了用户ID、邮箱甚至权限列表这些内容虽然不是明文暴露给“篡改者”但会被任何能拿到Token的人Base64解码后直接读到。JWT的Payload是明文base64编码不是加密的。所以敏感信息绝不放进JWT这条是铁律。第三存储与传输的要求提高了。Token需要客户端自己保存前端存哪里是个大问题。存localStorageXSS进攻后会被脚本直接偷走存内存变量刷新页面就丢存Cookie又要考虑CSRF和跨域。没有银弹只能按威胁模型权衡。4.3 Token失效议题为什么“注销”在无状态世界里变得棘手我处理线上问题见过最多的一类就是“Token失效”相关用户退出后拿着旧Token继续调用成功或者Token泄露了却无法远程吊销。这在无状态设计里是结构性问题因为你服务器根本没存这张票自然无法作废。业界对付这个问题的方案常见的有几种我按推荐度排序说。方案一做Token黑名单。服务器端维护一个已注销Token的列表通常是Redis set请求进来先查一下黑名单。这等于又把无状态拉回半有状态但它比全量Session轻量得多因为黑名单只记录“坏票”不记录“好票”。方案二缩短Token过期时间。把access token设成15分钟、30分钟让风险窗口变小同时用refresh token来做续签。方案三引入版本号。用户信息表里加一个token_version字段密码修改、账号封禁时让版本号1签发Token时把版本号写进去验签时比对版本号一旦发现版本不符直接拒绝。这个方案很实用尤其适合同一个账号在不同设备上登录的场景。5. JWT自带语义的签名票据好处和代价同样明显JWT全称JSON Web Token目前最火的Token形态。它和普通Token最大的区别在于三个字自包含。也就是说一张JWT里已经写好了“你是谁、你有哪些权限、什么时候到期”服务器不必查数据库就能知道你是什么角色。这让它在微服务网关鉴权、跨系统SSO的场景里特别吃香。5.1 JWT三段式的“身份证”结构拆解JWT长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c中间用两个点.分成三段Header、Payload、Signature。Header和Payload都是先转成JSON再做Base64URL编码。注意是Base64URL不是标准Base64因为标准Base64里的、/、在URL里会被转义JWT用-和_替代了和/去掉了填充符这套细节其实很容易被人忽略但你自己手写解析器时就会遇到。Header规定用什么算法签名Payload里才是真正有用的业务字段比如sub主题一般是用户ID、iat签发时间、exp过期时间、aud受众等这些字段名是有规范约定的。签名段则是用Header里声明的算法拿一个只有服务端知道的密钥对“Header.Payload”两部分做摘要后生成的。验签时做同样的计算比对结果是否一致就能确认内容没有被篡改过。网上搜“jwt漏洞总结”能找到大量案例这些漏洞几乎都集中在实现细节上下面单独开一节讲。5.2 签名算法HS256与RS256的选型逻辑签名算法这块我强烈建议你选型前先想清楚。JWT最常用的两种对称/非对称算法是HS256和RS256它们的信任模型完全不同。HS256是对称签名签发和验证用的是同一个密钥通常是一串很长的随机字符串。它的优点是计算快、实现简单缺点是签发方和验证方必须共享同一个密钥这在单一服务内部没问题但如果你有多个微服务每个服务都要持有这把密钥一旦某个服务被攻破密钥泄露攻击者就能自己签发任意身份的Token。RS256是非对称签名私钥在认证中心手里各个业务服务只持有公钥做验签。私钥不落地到业务服务安全性高一个量级特别适合“一个统一认证中心给多个后端服务发Token”的架构也是目前行业推荐做法。选型时还有一个隐藏细节RS256验签速度比HS256慢需要读取公钥所以很多人在性能和方便上妥协选了HS256。如果你在内部两个老服务之间做简单鉴权不跨团队共享密钥HS256能接受但只要可能被多个服务验证直接上RS256。另外一个非常重要的安全补丁是验签时必须强制校验Header里的alg字段明确白名单只允许服务端制定的算法。历史上著名的JWT算法混淆漏洞原理就是攻击者把alg改成none或改成HS256利用服务端的密钥误用来伪造Token。5.3 JWT漏洞面复盘算法混淆、弱密钥与过期校验我把JWT最常见的三类漏洞展开讲因为相关热搜词里“jwt漏洞总结”搜索量很高说明这是个刚需话题。第一类algnone攻击。有些JWT库默认允许不签名攻击者把Header里的alg改成none删掉签名段服务端验签时如果没禁掉none模式就会直接把Token当作合法。防御就一句明确禁止none算法验签失败直接拒绝。第二类弱密钥爆破。HS256的密码学强度完全取决于密钥的随机性和长度。我看到过有项目直接把密钥设成s3cr3t或者123456这种密钥分分钟被暴力破解。攻击者拿到一个合法Token用字典里的密钥去试一旦出签名一致就直接拿到密钥从此可以为所欲为。HS256密钥建议至少256位32字节的随机字节串并且定期轮换。第三类过期时间校验缺失或依赖客户端时间。JWT的exp字段是数字时间戳很多初学者在解析JWT时只校验了签名忘了比较exp和当前时间或者用了系统本地时间服务端时钟不准就出现“明明没过期结果报失效”的怪事。这里我建议统一用UTC时间戳并且校验尽量放在最早的网关层别等到业务代码里再判断。5.4 续签的正确姿势refresh token与滑动过期JWT过期了之后怎么办这是新手最容易卡住的地方。你不可能让用户每15分钟重新输一遍密码所以需要refresh token机制。我习惯的做法是用户登录成功后同时签发两个Token——短期的access token15分钟到2小时按系统敏感度定和长期的refresh token7到30天存数据库或Redis。access token用于每次请求的业务鉴权refresh token只用于一个场景当access token过期时客户端拿refresh token去换取新的access token。refresh token泄露的风险一样存在所以要在换发新Token时验证它有没有被使用过业内管这个叫“refresh token轮换”每次使用旧refresh token换新Token时立刻把旧refresh token作废再下发一个新的refresh token。这样即使某个refresh token泄露一次攻击者用的时候它会失效用户再刷新时能感知到异常。滑动过期指的是只要用户在持续活跃就顺延过期时间。比如JWT原本30分钟过期用户在29分钟时又请求了一次服务器签发一个新的30分钟Token用户无需重登。这个体验比固定过期好很多。但注意滑动过期会让Token理论上“永不过期”你在设计时最好加一个绝对时间上限比如最长连续登录7天超过之后强制重新登录兼顾安全和体验。6. 选型决策什么时候用谁以及我踩过的坑讲到这里四个概念的原理都清楚了但很多读者真正想问的还是那句实际项目里我到底该用哪个这一节我给你一套可落地的判断标准也顺带盘点一下我看过的各种“混用翻车”案例。6.1 四者对照表状态存储、安全边界、适用场景先放一张对照表方便你复制到自己的笔记里。技术状态存哪是否可被客户端篡改服务端能否主动失效典型场景Cookie客户端可被篡改需配合签名/会话方案不能单独失效需配合服务端承载session id、偏好设置Session服务端客户端只持有随机id篡改无效能销毁session即可传统Web登录、后台管理Token普通视实现而定不透明Token可防篡改自包含需验签不透明Token可删自包含Token难开放平台API鉴权、移动端JWT客户端可通过签名防篡改但Payload可被读难需黑名单/版本号辅助微服务鉴权、SSO、无状态接口从表格能看出一个重要事实没有绝对的“谁替代谁”。Cookie和Session是天然搭档Token可以用Session的风格存储也可以用JWT的风格无状态化JWT是Token的一种格式。我见过最健康的项目是登录入口用Session管后台员工开放平台用JWT发第三方Token两者共存互不干扰。6.2 常见误区一条条拆下面这些误区我几乎每轮面试都能遇到也是网上搜索关键词“cookie和session和token详解”时最大的信息噪音来源。误区一Cookie就等于Session。不是的。Cookie只是浏览器存储的载体Session是服务端的状态。你可以不用Cookie自己把session id放在请求头Authorization里传给服务端Session照样成立。反过来你也能用Cookie存JWT。两者维度不同别混为一谈。误区二Token一定比Session安全。这个说法我真听过很多次但完全站不住脚。JWT一旦泄露在过期前谁拿到都能用Session泄露了至少你还能在服务端把这个session删掉让凭证立即失效。安全性的关键是有没有传输加密、有没有校验、有没有过期机制、泄露后能不能撤回而不是“用了哪个技术名词”。误区三把登录Token存在localStorage就万事大吉。localStorage被XSS一锅端的案例太多了存Token更安全的做法是放在内存变量或HttpOnly Cookie里配合CSRF防护措施。如果你用的是SPA且接口跨域HttpOnly Cookie要处理CORS凭证问题麻烦一点但安全收益值得。误区四JWT里可以随便放用户密码、手机号。刚才强调过Payload是明文Base64任何人控制台解码就能看到。放身份证号这类敏感信息等于裸奔。JWT里只放“够用”的声明比如用户ID、角色、过期时间其他信息有需要再调接口查。误区五Session过期时间就是绝对时间。很多团队直接不设置滑动过期线上出现“用户写着写着报表就被踢下线”的投诉。正确做法是中间件里记录最后一次活跃时间超过阈值再失效同时加一个绝对上限。6.3 给初学者的最终建议如果你是第一次设计登录模块我的粗暴建议是这样能帮你少走弯路。第一单体应用、后台管理系统优先SessionCookie。它实现成本低服务端能主动踢人技术栈成熟出了问题排查路径短。第二前后端分离并且有移动端、小程序多端接入的优先考虑“Session/Redis 不透明Token”或者JWT方案。纯JWT要接受“无法主动下线”的现实如果产品确实需要“强制下线”“踢人”“在线设备管理”老老实实把状态存Redis别硬上无状态。第三统一认证中心、多个后端服务共享登录态选JWTRS256。它让业务服务不用每次去认证中心查会话直接在网关层验签放行链路短、延迟低。第四凡是做支付、改密码、删数据这类高危险操作无论你底层用的什么方案都要有独立的二次校验比如验证码、密码确认、短期操作Token不要只依赖会话状态本身。我自己的项目里踩坑最多的地方反而不是选型本身而是方案切换期。有一次从Session平滑迁移到JWT老用户的Cookie里还是session id新逻辑又只认JWT结果线上大批用户无故掉线。后来说白了就是没做兼容读取逻辑先判断请求头里有没有JWT字段没有就回落校验老session两边共存一个上线周期等旧Token全部自然过期再切换才能做到用户无感。最后再分享一个我长期保持的习惯所有涉及登录鉴权的代码不只是要写“能跑通”的happy path更要在代码注释里写明白“为什么这样选型”。三个月后你再回头看会发现当初写下的那些关于失效策略、安全边界、续签方案的决策原因才是这个系统真正值钱的部分。因为技术方案永远在变但“为什么做这个决策”的思考方式才是你下一次遇到问题时最快的导航。