你有没有遇到过这种情况:本地用 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:把状态“装进”令牌里
JWT(JSON Web Token)是近年最流行的认证方案。它和 Session 最大的区别是:Session 把状态存在服务器,JWT 把状态存在客户端。
一个 JWT 长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InpoYW5nc2FuIiwiaWF0IjoxNzE3MDAwMDAwLCJleHAiOjE3MTcwMDM2MDB9.s0meSignAture用.分成三段:
- Header:声明签名算法和令牌类型。
- Payload:存放业务数据,比如用户 ID、过期时间,Base64Url 编码,注意是明文可解的,不能放密码等敏感信息。
- Signature:把前两段用密钥签名,防止内容被篡改。
服务器校验 JWT 时,用同样的密钥和算法对前两段重新签名,比对签名是否一致,再检查exp过期时间,就能确认令牌合法。整个过程不需要去数据库查 session,天然适合分布式、微服务场景——这就是 JWT 能“革”Session 命的核心原因。
但 JWT 也有明显的短板:无法主动失效。用户注销、修改密码、被踢下线,只要 token 没过期,服务端拿它没办法。所以 JWT 不是银弹,后面第 4 部分我会专门讲怎么用双 token、黑名单等手段弥补这个缺陷。
下面用一张表把三者的核心差异列清楚,面试时照这个思路答基本就不会乱:
| 维度 | Cookie | Session | JWT |
|---|---|---|---|
| 存储位置 | 浏览器 | 服务器 | 客户端(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_ID=abc123; Path=/; Domain=example.com; Max-Age=3600; HttpOnly; Secure这里踩过最大的坑是Domain 设置不一致。如果登录接口在api.example.com,却把 Domain 设为example.com,那前端从www.example.com发起的请求确实能带上 cookie;但如果后端只返回了Domain=api.example.com,前端页面域名是www.example.com,cookie 就带不过去。线上报障里“我明明登录成功了,为什么刷新就掉了”,八成是这个问题。
2.2 跨域请求时 Cookie 丢失:CORS 和 credentials 的配合
前后端分离架构下,前端在localhost:3000,后端接口在api.example.com,这就产生了跨域。浏览器同源策略会拦截跨域请求读取响应,但如果你做了 CORS 处理,请求本身是可以发出去的。
问题是:默认情况下,跨域请求不会携带目标域名的 cookie。要让 cookie 跨域携带,必须同时满足三个条件:
- 前端发起请求时设置
withCredentials: true(axios 是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改成了Lax,98 前后又进一步收紧了对第三方 Cookie 的处理。
SameSite有三级:
- Strict:任何跨站请求都不带 cookie,最安全但用户体验差,从别的站跳转过来会导致未登录。
- Lax:只有顶级导航的 GET 请求(比如点链接跳转)携带 cookie,跨站的 iframe、XHR、fetch 都不带。
- None:所有跨站请求都带,但必须同时设置
Secure,也就是必须走 HTTPS。
Chrome 默认 Lax 之后,很多旧项目里跨站嵌入的登录态就失效了。比如一个页面通过 iframe 嵌入了另一个域名的功能,iframe 里的同步请求以前能带 cookie,现在直接不带。
排查这类问题我有个固定的三步套路:
- 打开 DevTools → Application → Cookies,先确认目标域名下有没有 cookie,cookie 的
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 Session) | Nginx 按用户 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> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>配置文件里指定 Session 存储方式和 Redis 连接:
spring.session.store-type=redis spring.data.redis.host=127.0.0.1 spring.data.redis.port=6379 spring.session.timeout=30m这样配置好后,原来代码里的HttpSession用法完全不用改,框架会自动把session.setAttribute()的数据序列化到 Redis,key 是spring:session:sessions:{sessionId},并设置对应的过期时间。
实操中要特别注意Redis 中的 Session 序列化方式。默认的 JDK 序列化会把对象类型信息也存进去,Redis Desktop Manager 里看到的是乱码,排查问题很费劲。建议自定义 RedisSerializer,用 Jackson JSON 序列化:
@Bean public RedisSerializer<Object> 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 的鞋印原理:为什么别人改不了你的 Token
JWT 的签名机制,用一句话说就是:用服务端才知道的密钥,给 token 内容盖一个“防伪印章”。客户端拿到 token 后,如果好奇地把 Payload 里的userId从 1 改成 2,由于他不知道密钥,没法重新算出匹配的 Signature,服务端用密钥一验就算出来签名对不上,直接拒绝。
签名算法最常见的是 HMAC SHA256(HS256),它是对称密钥,签发和校验用同一个密钥。选型时有个很多人忽略的点:如果你有多个服务各自独立校验 JWT,每个服务都要持有这个密钥,密钥一泄漏,攻击者就能签发任意身份 token;所以大型系统更推荐 RS256,它是非对称的,签发用私钥,校验用公钥,公钥可以安全地分发给各个服务。
下面用 Java(jjwt)写一个完整的签发和校验示例,这套代码我项目里直接改改就能用:
// 生成 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 { Jws<Claims> 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 登录后携带 Token:Header 里走,别放 URL
JWT 签发之后,前端要怎么存、请求时怎么带?建议是:
- 存储:优先放内存变量或 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)存进 Redis,TTL 设为 token 剩余有效期。校验时先查黑名单,命中就拒绝。实现简单、精准,但每个请求都多一次 Redis 查询。 - 短过期 + 刷新:把 Access Token 时间设得很短,即使泄漏,风险窗口也小。配合双 Token,用户无感续期。
- 版本号:用户表里存
token_version,JWT 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 读取(除非 HttpOnly);Session 存在服务器内存或 Redis,更安全可控,但占用服务端资源。 第四句给场景:单纯记录用户偏好用 Cookie;登录态、敏感数据放 Session;前后端分离、分布式场景用 JWT。
最后如果面试官追问“JWT 和 Session 怎么选”,答一句“Session 适合服务端渲染的传统应用,JWT 适合前后端分离和开放 API”就很完整了。
顺带提一个 Java 基础变体题:“用三种修饰符修饰 List,List 中的值还能修改或删除吗?”这里的坑在于final、static、transient修饰的是引用而不是内容。final List只是不能重新赋值引用,list 本身依然能 add/remove;static让它变成类级别共享;transient只是序列化忽略。真正要做到不可修改得用Collections.unmodifiableList()或List.of()。这个点经常和 Session 里存共享数据、并发修改的问题一起考,建议顺手掌握。
5.5 项目里到底用 Session 还是 JWT:一个讲原则的选型表
最后总结一下技术选型,不搞一刀切,直接给判断标准:
| 项目情况 | 推荐方案 | 理由 |
|---|---|---|
| 传统服务端渲染、单体 Web 应用 | Session + Cookie | 简单可靠,成熟稳定 |
| 前后端分离、多个前端(Web/小程序/App) | JWT 或 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 头),再确认凭证内容对不对、后端认不认。大部分认证故障都能靠这两步定位个八九不离十。希望这篇博文能帮你少走一些弯路,至少下次再遇到“用户莫名其妙掉线”的时候,心里能有个清晰的排查地图。