ARTICLE DETAIL

资讯详情

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

Cookie、Session、JWT三兄弟:从原理到实战,一篇讲透认证与状态管理

Cookie、Session、JWT三兄弟:从原理到实战,一篇讲透认证与状态管理 你有没有遇到过这种情况本地用 Chrome 调试登录功能一切正常一上线就死活登不进去或者明明在 A 域名下已经登录了跳到 B 域名又变回游客再或者前后端分离的项目里后端明明把 session 建立好了前端 request 里就是看不到 Set-Cookie 头。我这些年排查线上问题有一半以上的时间都耗在 session、cookie、Jwt-token 这三兄弟身上。这三个词是 Web 开发的“地基”也是面试必考、实战必踩的坎。但市面上讲它们的文章要么太浅只给结论不给原理要么太散每个概念单独讲串不起来。这篇博文我想顺着“HTTP 无状态 → Cookie 存储 → Session 会话 → JWT 无状态令牌”这条线把三者的原理、区别、实战操作、常见坑一次讲透顺便把分布式 session 共享、订单过期处理、死锁、熔断降级这些面试和线上经常碰到的问题也一起捋一遍。内容适合正在做 Web 后端的朋友也适合准备面试的同学还有那些被线上 cookie 丢了、session 失效折磨过的人。1. 三兄弟到底在解决什么问题1.1 HTTP 本身就是个“失忆症”患者先把最底层的东西摆出来HTTP 协议是无状态的。你向服务器发一个请求服务器处理完返回响应这件事就结束了。服务器不会记得上一次请求你是谁、干了什么。这就带来一个很严肃的问题——用户登录之后下一次请求怎么证明“我还是我”如果没有任何机制那每次请求都要重新输入用户名密码网页版产品根本没法用。所以我们需要一种“状态管理”方案。而 session、cookie、JWT 本质上都是围绕“怎么让 HTTP 有记忆”这件事设计的方案只是记忆存放的位置和方式不一样。用一个生活化的类比你去一家健身房第一次去办了卡前台登记了你的资料。之后每次进门你有两种方式证明身份第一种前台在你自己手臂上盖一个荧光章每次进门让前台扫一下章章里的编号对应前台本子上的记录。这个章就是 cookie前台的记录本和核对流程就是 session。第二种前台给你一张加密的会员卡卡片里直接写着“张三有效期到年底”每次进门刷卡读卡器能验证这张卡是健身房产出的、内容没被改过。这张卡就是 JWT。理解了这两个场景后面所有细节都会顺理成章。1.2 Cookie浏览器里的“便利贴”Cookie 是服务器下发给浏览器的一小段文本浏览器会把它存起来之后每次向同一域名发起请求都会自动把这个文本放进请求头里带过去。Cookie 本身有几个非常关键的属性决定了它的行为和安全性Domain限定 cookie 在哪些域名下有效。比如.example.com可以匹配a.example.com和b.example.com。Path限定 cookie 在哪个路径下有效默认/。Max-Age/Expires过期时间。不设置的话就是 Session Cookie浏览器一关就没。HttpOnly设为 true 后JavaScript 的document.cookie拿不到这个 cookie可以有效防止 XSS 脚本窃取会话信息。Secure只有 HTTPS 连接才会携带这个 cookie。SameSite控制第三方请求是否携带本域名 cookie后面讲 Chrome 不携带 cookie 时重点说它。还有一个经常被忽略的细节cookie 里不建议直接存中文。因为 HTTP 头规范里对字符集有限制中文和其他 Unicode 字符直接放进 cookie 可能导致解析异常。正确做法是用encodeURIComponent编码后再存读取时再解码。之前有个朋友做登录功能把用户名直接塞进 cookie结果带特殊字符的用户名一登录后续请求 header 直接报错找了大半天才定位到是编码问题。1.3 Session服务器端的“小本本”Session 是存储在服务器端的会话数据。用户第一次访问时服务器创建一个 Session 对象生成一个唯一的 Session ID把 Session ID 通过 Set-Cookie 下发给浏览器。浏览器后续请求带上这个 Session ID服务器就能通过它找到对应的 Session 数据。所以 Session 和 Cookie 不是竞争关系而是配合关系Session 的数据在服务端Session ID 的传递靠 Cookie。当然如果浏览器禁用 Cookie也可以用 URL 重写的方式把 Session ID 拼在 URL 后面但这种方式不安全也不优雅容易在日志里泄漏会话标识不建议使用。Session 默认的过期时间各容器不一样Tomcat 默认是 30 分钟。注意这个时间是从最后一次访问开始算的不是从创建时间开始算的。比如用户登录后一直操作Session 永远不会超时用户登录后关掉浏览器走人30 分钟后 Session 才会失效。1.4 JWT把状态“装进”令牌里JWTJSON Web Token是近年最流行的认证方案。它和 Session 最大的区别是Session 把状态存在服务器JWT 把状态存在客户端。一个 JWT 长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InpoYW5nc2FuIiwiaWF0IjoxNzE3MDAwMDAwLCJleHAiOjE3MTcwMDM2MDB9.s0meSignAture用.分成三段Header声明签名算法和令牌类型。Payload存放业务数据比如用户 ID、过期时间Base64Url 编码注意是明文可解的不能放密码等敏感信息。Signature把前两段用密钥签名防止内容被篡改。服务器校验 JWT 时用同样的密钥和算法对前两段重新签名比对签名是否一致再检查exp过期时间就能确认令牌合法。整个过程不需要去数据库查 session天然适合分布式、微服务场景——这就是 JWT 能“革”Session 命的核心原因。但 JWT 也有明显的短板无法主动失效。用户注销、修改密码、被踢下线只要 token 没过期服务端拿它没办法。所以 JWT 不是银弹后面第 4 部分我会专门讲怎么用双 token、黑名单等手段弥补这个缺陷。下面用一张表把三者的核心差异列清楚面试时照这个思路答基本就不会乱维度CookieSessionJWT存储位置浏览器服务器客户端token 本身是否占服务端资源不占占不占跨域支持受限同源策略受限依赖 Cookie 传 ID天然支持主动失效可删可删难需额外机制安全性可能被 XSS 窃取依赖 Session ID 保护依赖密钥保护Payload 明文分布式友好度一般差需共享好2. Cookie 实战从下发到携带跨越那些隐藏在 Header 里的坑2.1 用 Set-Cookie 正确下发 Cookie 的姿势后端设置 cookie本质就是设置Set-Cookie响应头。不管是 Java 的HttpServletResponse#addCookie、Node 的res.setHeader(Set-Cookie, ...)还是框架封装好的cookie()方法最终落到 HTTP 层都一样。实际项目中我建议生产环境至少这样设置HttpOnly必须开Secure在 HTTPS 下必须开SameSite根据场景选Lax或None。用框架伪代码表示Cookie sessionCookie new Cookie(SESSION_ID, sessionId); sessionCookie.setHttpOnly(true); sessionCookie.setSecure(true); sessionCookie.setPath(/); sessionCookie.setMaxAge(3600); // 1小时 sessionCookie.setDomain(example.com); response.addCookie(sessionCookie);对应到 HTTP 头就是Set-Cookie: SESSION_IDabc123; Path/; Domainexample.com; Max-Age3600; HttpOnly; Secure这里踩过最大的坑是Domain 设置不一致。如果登录接口在api.example.com却把 Domain 设为example.com那前端从www.example.com发起的请求确实能带上 cookie但如果后端只返回了Domainapi.example.com前端页面域名是www.example.comcookie 就带不过去。线上报障里“我明明登录成功了为什么刷新就掉了”八成是这个问题。2.2 跨域请求时 Cookie 丢失CORS 和 credentials 的配合前后端分离架构下前端在localhost:3000后端接口在api.example.com这就产生了跨域。浏览器同源策略会拦截跨域请求读取响应但如果你做了 CORS 处理请求本身是可以发出去的。问题是默认情况下跨域请求不会携带目标域名的 cookie。要让 cookie 跨域携带必须同时满足三个条件前端发起请求时设置withCredentials: trueaxios 是axios.defaults.withCredentials true。后端响应头设置Access-Control-Allow-Credentials: true。后端响应头里的Access-Control-Allow-Origin不能是*必须是明确的源地址比如https://www.example.com。很多人在条件 3 上翻车后端图省事配了*结果单独开Allow-Credentials时浏览器直接报错因为规范里就禁止*和 credentials 共存。这种问题的表现是单测接口用 Postman 一切正常浏览器里请求被 CORS 拦下控制台报错信息里夹着一句 “The value of the Access-Control-Allow-Origin header ... must not be the wildcard”。顺带一提如果你在做移动端真机调试有时浏览器会弹出 “pending authentication: please accept debugging session on the device” 之类的提示这是浏览器在请求设备上的调试授权点允许即可跟 cookie 本身没关系但不少新手会误以为是自己代码导致的卡顿。2.3 Chrome 98 之后 Cookie 不携带的排查实录Chrome 98 那段时间大量项目突然反馈“登录失效”“Cookie 丢了”。其实 Chrome 从 80 版本开始就把SameSite的默认值从None改成了Lax98 前后又进一步收紧了对第三方 Cookie 的处理。SameSite有三级Strict任何跨站请求都不带 cookie最安全但用户体验差从别的站跳转过来会导致未登录。Lax只有顶级导航的 GET 请求比如点链接跳转携带 cookie跨站的 iframe、XHR、fetch 都不带。None所有跨站请求都带但必须同时设置Secure也就是必须走 HTTPS。Chrome 默认 Lax 之后很多旧项目里跨站嵌入的登录态就失效了。比如一个页面通过 iframe 嵌入了另一个域名的功能iframe 里的同步请求以前能带 cookie现在直接不带。排查这类问题我有个固定的三步套路打开 DevTools → Application → Cookies先确认目标域名下有没有 cookiecookie 的SameSite和Expires是什么。切到 Network看请求的 Request Headers 里有没有Cookie字段。如果没有说明是“没带”如果有看是浏览器策略拦了还是后端没下发。如果带上了但后端不认查 Session ID 是否过期、cookie 值是否被截断或编码不一致。这套流程排查了不下二十次命中率极高。2.4 控制台里看不到 Set-Cookie别慌先查这几个点开发时经常遇到后端响应头里明明有Set-Cookie但 Application 面板里就是看不到 cookie或者请求里就是没带。除了前面说的跨域、SameSite 问题还有几个容易忽略的原因Max-Age 为负数或 0等同于告诉浏览器“把这个 cookie 删掉”响应会 200但 cookie 不会落盘。Domain 不匹配服务器设置了当前请求 Host 之外的 Domain浏览器会拒绝存储。HttpOnly 且非 HTTPS如果 Secure 属性和请求协议不匹配同样会被忽略。隐私模式/禁用 Cookie移动端浏览器、无痕模式下 cookie 行为更严格夸克这类浏览器的 cookie 管理入口藏在“设置 → 网站数据”里不像 PC 端 DevTools 那么直观排查时容易忽略“浏览器压根没存”这个前提。如果实在查不出来就用 curl 直接打接口看原始响应头curl -I https://api.example.com/login这样能一眼确认后端到底有没有下发 Set-Cookie以及下发值长什么样直接排除浏览器干扰。3. Session 的分布式困局与共享方案3.1 为什么负载均衡之下 Session 就“漂移”了单机部署时用户请求始终打到同一台机器Session 存在本地内存里一切安好。但一旦上了 Nginx 负载均衡、微服务多实例部署同一个用户的请求可能轮流打到 A 机器和 B 机器。第一次请求在 A 机器创建了 Session第二次请求被分到 B 机器B 机器内存里没有这个 Session用户就被当成新访客于是又走一遍登录流程。这就是经典的Session 漂移问题。解决办法有不少但复杂度差异很大。3.2 四种主流 Session 共享方案对比我把方案按推荐程度从低到高排一下方案原理优点缺点适用场景粘性会话Sticky SessionNginx 按用户 IP 或某种规则固定分发到同一台机器实现最简单零改造单点故障机器重启就丢 Session扩容不灵活小规模、可接受短时掉线Session 复制各节点之间同步 Session 数据任意节点可用故障可切换数据冗余大、同步有延迟节点一多网络开销爆炸节点少2-3 台的旧系统Redis 集中存储所有节点都从 Redis 读 Session 数据无状态、扩容方便、支持过期策略引入额外组件Redis 挂了全站受影响需要高可用绝大多数现代分布式系统前端 Token 化服务端不存状态JWT 或自签 Token 由客户端持有完全不占服务端内存天然无状态登出被动、密钥管理要求高token 泄漏风险前后端分离、开放 API顺序基本就是项目演进路线。我的个人建议是新项目除非极简单否则直接上 Redis 共享 Session 或 JWT不要在粘性会话上省事。3.3 Spring Session Redis 落地配置Spring Boot 项目做 Redis 共享 Session 非常简单核心就是引入 Spring Session Redis然后配置一个存储类型。依赖如下dependency groupIdorg.springframework.session/groupId artifactIdspring-session-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency配置文件里指定 Session 存储方式和 Redis 连接spring.session.store-typeredis spring.data.redis.host127.0.0.1 spring.data.redis.port6379 spring.session.timeout30m这样配置好后原来代码里的HttpSession用法完全不用改框架会自动把session.setAttribute()的数据序列化到 Rediskey 是spring:session:sessions:{sessionId}并设置对应的过期时间。实操中要特别注意Redis 中的 Session 序列化方式。默认的 JDK 序列化会把对象类型信息也存进去Redis Desktop Manager 里看到的是乱码排查问题很费劲。建议自定义 RedisSerializer用 Jackson JSON 序列化Bean public RedisSerializerObject springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); }改完之后Session 里的对象必须保证有空构造方法和 getter/setter否则 JSON 反序列化时会报类型转换错误。这也是 Spring Session 踩坑率比较高的一个点。3.4 从“会话过期”延伸到“订单过期”后台任务怎么设计Session 有过期时间订单、优惠券、验证码这些业务数据也有过期时间。热搜词里有一道很经典的面试题“订单过期了怎么办”正好和会话管理是同一套思维。处理订单过期的常见做法有五种定时轮询数据库起一个定时任务每分钟扫描一次订单表把超时未支付的订单取消。实现最简单但存在任务延迟订单量大时数据库压力大。延迟队列下单后把订单编号扔进延迟队列RabbitMQ 的 TTL死信队列或者 Redisson 的延时队列过期后由消费者处理。实时性最好但需要引入 MQ 或 Redis。Redis 过期回调给订单 Key 设置过期时间利用 Redis 的键空间通知订阅过期事件。够轻量但 Redis 的过期事件并不保证准时而且 key 满了可能被淘汰可靠性一般。惰性检查用户查询订单时再判断超没超时。零额外成本但订单状态要等用户触发才更新后台统计不准。时间轮Timing Wheel高性能的定时任务调度方案适合高吞吐、大量超时任务的场景实现复杂度最高。面试时答到“延迟队列 定时任务补偿”的组合基本就是满分答案延迟队列负责及时取消状态定时任务负责兜底扫描防止消息丢失导致脏数据。很多订单系统实际也是这么设计的。4. JWT-Token 实战从签发、校验到续期把方案落到代码4.1 JWT 的鞋印原理为什么别人改不了你的 TokenJWT 的签名机制用一句话说就是用服务端才知道的密钥给 token 内容盖一个“防伪印章”。客户端拿到 token 后如果好奇地把 Payload 里的userId从 1 改成 2由于他不知道密钥没法重新算出匹配的 Signature服务端用密钥一验就算出来签名对不上直接拒绝。签名算法最常见的是 HMAC SHA256HS256它是对称密钥签发和校验用同一个密钥。选型时有个很多人忽略的点如果你有多个服务各自独立校验 JWT每个服务都要持有这个密钥密钥一泄漏攻击者就能签发任意身份 token所以大型系统更推荐 RS256它是非对称的签发用私钥校验用公钥公钥可以安全地分发给各个服务。下面用 Javajjwt写一个完整的签发和校验示例这套代码我项目里直接改改就能用// 生成 JWT String secret your-256-bit-secret; String token Jwts.builder() .setSubject(userId.toString()) .claim(username, username) .claim(role, admin) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 30 * 60 * 1000)) .signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256) .compact();校验try { JwsClaims jws Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes())) .build() .parseClaimsJws(token); Claims claims jws.getBody(); Long userId Long.valueOf(claims.getSubject()); // 继续处理业务 } catch (ExpiredJwtException e) { // token 过期 } catch (JwtException e) { // 签名非法或 token 被篡改 }注意顺序先验签名再验过期。有些框架代码会把过期校验放在签名校验前面这样攻击者可以伪造一个“过期 token”来探测签名算法细节属于不安全的写法。4.2 登录后携带 TokenHeader 里走别放 URLJWT 签发之后前端要怎么存、请求时怎么带建议是存储优先放内存变量或 Pinia/Redux 中刷新页面会丢失所以需要配合持久化。放localStorage最容易被 XSS 读到放sessionStorage关闭标签页就没实际项目里很多人图省事直接塞localStorage安全性确实差一点。更稳的做法是短 token 放内存、刷新用 refresh token 放 HttpOnly cookie但实现复杂度高。携带放Authorization: Bearer token请求头。一定不要放 URL 查询参数里因为 URL 会被浏览器历史、服务器访问日志、代理网关记录下来token 等于裸奔。前端 axios 加一个请求拦截器统一注入axios.interceptors.request.use(config { const token getToken(); if (token) { config.headers.Authorization Bearer ${token}; } return config; });4.3 Access Token 过期了怎么办双 Token 续期方案JWT 最多痛点就是“过期”。设太短用户用着用着突然要重新登录设太长token 泄漏后的风险窗口太大。业界比较通用的解法是Access Token Refresh Token 双 Token 方案Access Token有效期短15 分钟2 小时用于业务接口鉴权。Refresh Token有效期长7 天30 天只用于调用刷新接口换取新的 Access Token。它可以存数据库或 Redis做到可主动吊销。用户 Access Token 过期后前端拿到 401自动调用刷新接口/auth/refresh用 Refresh Token 换新 Access Token。刷新接口返回后前端重放刚才失败的请求用户无感知。Refresh Token 本身也是一个 JWT但它的 Payload 里只放 refresh_token 的 ID不放用户业务数据。实现时要注意并发刷新的问题如果同一时间有多个请求都返回 401会同时调用刷新接口可能把 Refresh Token 刷新多次。常见做法是前端做请求队列在刷新期间把其他请求暂存刷新成功后再统一重放。这个细节不做的话线上偶尔会出现“登录状态正常但偶发 401”的怪问题。4.4 JWT 的注销困境黑名单、短过期和版本号接 4.3 往下走JWT 无法主动失效的问题实际项目里有三种补救黑名单用户注销时把 token 的jti唯一 ID存进 RedisTTL 设为 token 剩余有效期。校验时先查黑名单命中就拒绝。实现简单、精准但每个请求都多一次 Redis 查询。短过期 刷新把 Access Token 时间设得很短即使泄漏风险窗口也小。配合双 Token用户无感续期。版本号用户表里存token_versionJWT Payload 里也带ver。用户改密码、被踢下线时把版本号 1校验时比对不一致就拒绝。缺点是多一次数据库查询。我的建议是普通业务用双 Token 短过期就够了涉及支付、账户安全等高敏感场景注销时再把 token 加入黑名单按风险等级做差异化策略。4.5 密钥管理不要在代码里写死密钥最后提醒一个看似基础但很多人犯的错JWT 密钥直接硬编码在 application.yml 里然后代码库传来传去最后上了生产也没换。密钥安全的基本要求长度至少 256 位HS256 算法对密钥长度有要求过短会直接报错。不同环境用不同密钥测试环境的密钥泄漏不能影响生产。密钥放环境变量或配置中心而不是 git 仓库。定期轮换密钥轮换时保证旧 token 有一段兼容期比如校验时新旧密钥都试一遍否则用户会集体掉线。5. 面试与技术选型必看死锁、熔断、注册发现与三件套抉择5.1 死锁产生的四个条件以及怎么预防死锁这个问题和认证授权关系不大但面试频率极高而且经常和并发场景绑在一起考。死锁产生的四个必要条件我建议背熟互斥资源同一时刻只能被一个线程占用。持有并等待线程持有资源 A同时等待资源 B。不可剥夺资源只能由持有者自己释放。循环等待线程 1 等线程 2 的资源线程 2 等线程 1 的资源。对应的避免策略也正好四板斧尽量使用无锁编程或原子操作打破互斥。一次性申请所有资源申请不到就释放已有资源打破持有并等待。设置超时时间超过一定时间自动释放。对资源按固定顺序加锁所有线程都按同一顺序请求打破循环等待。面试升级版会问“怎么定位死锁”。Java 里的做法是jstack导出线程栈搜 “deadlock” 关键字或者用jconsole的线程检测功能。MySQL 里死锁则用SHOW ENGINE INNODB STATUS看 LATEST DETECTED DEADLOCK 段的两个事务分别持有什么锁、在等什么。5.2 秒杀场景下的服务熔断与降级秒杀是面试高频场景题里面必谈限流、熔断、降级三个词。很多人把熔断和降级混为一谈其实完全不同限流在入口处控制请求速率超出阈值直接拒绝。常用算法计数器、滑动窗口、令牌桶、漏桶。熔断当下游服务错误率超过阈值比如 5 分钟内错误率 50%打开熔断器后续请求快速失败不再调用下游给下游喘息时间。一段时间后进入半开状态放少量请求试探恢复情况。降级主动放弃非核心功能保证核心功能可用。比如秒杀详情页挂了可以降级成静态页评论服务挂了可以先不展示评论不让它拖垮下单流程。Spring Cloud 里常用 Sentinel 或 Resilience4j 来实现。Sentinel 的优势是规则可以动态下推控制台可视化配置适合业务频繁调整的团队Resilience4j 轻量适合不想引入重量级控制台的微服务。实际落地时熔断的阈值和半开时间一定要根据压测数据来定不能拍脑袋。之前见过一个团队把熔断阈值设成 10% 错误率结果一次小规模异常直接熔断了所有流量恢复后还因为半开时间太短反复触发差点酿成事故。5.3 SpringBoot 服务注册与发现微服务的基础设施“服务是多次部署的如何共享 Session”背后对应的就是微服务架构。服务注册与发现解决的是服务实例动态变化后调用方怎么找到目标实例的问题。主流方案组件类型特点Eureka注册中心AP经典但已停更自我保护机制常被吐槽Consul注册中心CP自带 KV 和健康检查raft 协议保证一致性Nacos注册中心配置中心AP/CP 可切换国内使用率高功能全社区活跃SpringBoot 集成 Nacos 很直接依赖加spring-cloud-starter-alibaba-nacos-discovery配置spring.cloud.nacos.discovery.server-addr启动类加EnableDiscoveryClient服务就能自动注册。调用方配合LoadBalanced RestTemplate 或 OpenFeign就能按服务名做负载均衡调用。和 Session 共享的关系是微服务将服务拆分后用户会话状态如果还留在某个实例内存里就会有服务漂移时用户掉线的风险。所以要么把 Session 抽到 Redis要么直接上 JWT 无状态认证。这也是为什么设计认证方案要在微服务改造之前就决策好不然后面返工成本极高。5.4 面试高频题速答Cookie 和 Session 的区别这道题几乎必考我提供一个能在一分钟内答完的高分框架第一句给本质Cookie 是存储在浏览器的数据Session 是存储在服务器的数据。 第二句给关系Session ID 通常通过 Cookie 传递两者是配合关系。 第三句给差异Cookie 本地存储不受服务端控制大小限制约 4KB可被 JS 读取除非 HttpOnlySession 存在服务器内存或 Redis更安全可控但占用服务端资源。 第四句给场景单纯记录用户偏好用 Cookie登录态、敏感数据放 Session前后端分离、分布式场景用 JWT。最后如果面试官追问“JWT 和 Session 怎么选”答一句“Session 适合服务端渲染的传统应用JWT 适合前后端分离和开放 API”就很完整了。顺带提一个 Java 基础变体题“用三种修饰符修饰 ListList 中的值还能修改或删除吗”这里的坑在于final、static、transient修饰的是引用而不是内容。final List只是不能重新赋值引用list 本身依然能 add/removestatic让它变成类级别共享transient只是序列化忽略。真正要做到不可修改得用Collections.unmodifiableList()或List.of()。这个点经常和 Session 里存共享数据、并发修改的问题一起考建议顺手掌握。5.5 项目里到底用 Session 还是 JWT一个讲原则的选型表最后总结一下技术选型不搞一刀切直接给判断标准项目情况推荐方案理由传统服务端渲染、单体 Web 应用Session Cookie简单可靠成熟稳定前后端分离、多个前端Web/小程序/AppJWT 或 Token 认证跨端携带方便服务端无状态微服务、需要多服务共享登录态Redis 共享 Session 或 JWT避免 Session 漂移扩展性好高安全性业务支付、后台管理Session 严格 Cookie 属性或 JWT 黑名单可主动注销、可控性强开放平台 API给第三方调用JWT 或 OAuth2标准协议、易集成还有一个容易忽略的原则不要混用两套认证体系。有些项目在同一个服务里既支持 Session 又支持 JWT两套过滤器、两种用户态结果逻辑分支爆炸排查问题要同时看 Cookie 和 Authorization 头非常痛苦。如果真的需要平滑过渡也建议用适配器模式统一封装而不是散落在各个 Controller 里判断。我自己实际做项目时如果是从零搭建会优先选Redis Session而不是 JWT不是说 JWT 不好而是很多内部管理系统里“主动踢人”“用户注销”是刚需Session Redis 的实现路径最短。但对外的开放 API、小程序登录JWT 又是更合适的底座。关键不是哪个技术更高级而是要充分理解各自的适用边界。从 Chrome 策略调整导致的 cookie 丢失到分布式环境下的 session 漂移再到 JWT 的过期与注销这些坑我基本都踩过一遍。最后分享一个排查认证问题的通用心法先看“有没有”再看“对不对”。先确认请求里有没有带凭证Cookie 或 Authorization 头再确认凭证内容对不对、后端认不认。大部分认证故障都能靠这两步定位个八九不离十。希望这篇博文能帮你少走一些弯路至少下次再遇到“用户莫名其妙掉线”的时候心里能有个清晰的排查地图。
返回列表