【架构实战】分布式会话:从Session到JWT的演进
2026/6/11 4:25:09 网站建设 项目流程

一、Session共享让我头大

2018年,我们从单机扩展到多实例部署。用户反馈"登录状态丢失"——在A机器登录,请求到了B机器就没登录了。

原因是:Session存在JVM内存中,每个实例的Session是独立的。

我们尝试了Session复制(Tomcat Cluster),但Session序列化和网络传输的开销太大。

后来又用了Spring Session + Redis,虽然解决了共享问题,但每次请求都要访问Redis,增加了延迟。

最终我们切换到了JWT,彻底告别了Session。


二、方案对比

┌─────────────────────────────────────────────────────────────────┐ │ 会话管理方案对比 │ │ │ │ 方案 │ 状态 │ 性能 │ 扩展性 │ 适用场景 │ │ ───────────────────────────────────────────────────────────── │ │ 本地Session │ 有状态 │ 高 │ 差 │ 单机 │ │ Session复制 │ 有状态 │ 差 │ 差 │ 小集群 │ │ Spring Session │ 有状态 │ 中 │ 中 │ 传统Web │ │ JWT │ 无状态 │ 高 │ 好 │ 微服务/移动端 │ │ Token+Redis │ 半无状态│ 中 │ 好 │ 需要主动失效 │ │ │ └──────────────────────────────────────────────────────────────────┘

三、JWT实现

3.1 Token生成与验证

/** * JWT工具类 */@Component@Slf4jpublicclassJwtTokenProvider{@Value("${jwt.secret}")privateStringsecret;@Value("${jwt.access-token-expiration:7200}")privatelongaccessTokenExpiration;@Value("${jwt.refresh-token-expiration:604800}")privatelongrefreshTokenExpiration;/** * 生成Access Token */publicStringgenerateAccessToken(UserDetailsuserDetails){Map<String,Object>claims=newHashMap<>();claims.put("userId",userDetails.getUserId());claims.put("username",userDetails.getUsername());claims.put("roles",userDetails.getRoles());returnJwts.builder().setClaims(claims).setSubject(userDetails.getUsername()).setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()+accessTokenExpiration*1000)).signWith(SignatureAlgorithm.HS512,secret).compact();}/** * 生成Refresh Token */publicStringgenerateRefreshToken(UserDetailsuserDetails){returnJwts.builder().setSubject(userDetails.getUsername()).setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()+refreshTokenExpiration*1000)).signWith(SignatureAlgorithm.HS512,secret).compact();}/** * 验证Token */publicJwtClaimsvalidateToken(Stringtoken){try{Claimsclaims=Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();returnJwtClaims.builder().userId(claims.get("userId",Long.class)).username(claims.getSubject()).roles((List<String>)claims.get("roles")).expiration(claims.getExpiration()).build();}catch(ExpiredJwtExceptione){thrownewJwtTokenExpiredException("Token已过期");}catch(JwtExceptione){thrownewJwtTokenInvalidException("Token无效");}}}

3.2 登录与Token刷新

/** * 认证服务 */@Service@Slf4jpublicclassAuthService{@AutowiredprivateJwtTokenProvidertokenProvider;@AutowiredprivateStringRedisTemplateredisTemplate;/** * 登录 */publicLoginResultlogin(LoginRequestrequest){// 验证用户名密码UserDetailsuser=authenticate(request.getUsername(),request.getPassword());// 生成TokenStringaccessToken=tokenProvider.generateAccessToken(user);StringrefreshToken=tokenProvider.generateRefreshToken(user);// 存储Refresh Token(用于主动失效)redisTemplate.opsForValue().set("refresh_token:"+user.getUserId(),refreshToken,7,TimeUnit.DAYS);returnLoginResult.builder().accessToken(accessToken).refreshToken(refreshToken).expiresIn(7200).build();}/** * 刷新Token */publicLoginResultrefreshToken(StringrefreshToken){JwtClaimsclaims=tokenProvider.validateToken(refreshToken);// 验证Refresh Token是否在Redis中Stringstored=redisTemplate.opsForValue().get("refresh_token:"+claims.getUserId());if(!refreshToken.equals(stored)){thrownewJwtTokenInvalidException("Refresh Token无效");}// 生成新的Access TokenUserDetailsuser=loadUser(claims.getUserId());StringnewAccessToken=tokenProvider.generateAccessToken(user);returnLoginResult.builder().accessToken(newAccessToken).expiresIn(7200).build();}/** * 登出(主动失效) */publicvoidlogout(LonguserId){// 删除Refresh TokenredisTemplate.delete("refresh_token:"+userId);// 将Access Token加入黑名单// (因为JWT无状态,Access Token在过期前仍然有效)// 这里使用Token黑名单方案log.info("用户登出: userId={}",userId);}}

四、踩坑实录

坑1:JWT无法主动失效

用户改了密码,但旧Token还能用。

解决:维护Token黑名单(Redis),或者缩短Access Token有效期+Refresh Token轮换。

坑2:JWT太大

JWT Payload塞了太多数据,Token超过4KB,超过Header限制。

解决:JWT只存必要信息(userId、username),其他信息按需查询。

坑3:Token存储不安全

前端把Token存在localStorage,被XSS攻击窃取。

解决:Token存在HttpOnly Cookie中,或使用加密存储。

坑4:跨域Token传递

前后端分离,跨域请求Token丢失。

解决:CORS配置允许携带凭证(withCredentials)。

坑5:并发刷新Token

同一用户的多个设备同时刷新Token,导致Token失效。

解决:Refresh Token加版本号,只允许最新的Token刷新。


五、总结

会话管理方案选型:

场景推荐
传统WebSpring Session + Redis
移动端/APIJWT
需要主动失效JWT + Redis黑名单
SSOOAuth2 + JWT

最佳实践:

  1. JWT只存必要信息
  2. Access Token短有效期(2小时)
  3. Refresh Token长有效期(7天)
  4. Token安全存储
  5. 做好Token刷新和失效

血的教训:

JWT不是万能的。无状态是优势也是劣势,选择方案前先想清楚你的业务需要什么。

思考题:你的系统用了什么会话管理方案?


个人观点,仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询