【架构实战】分布式会话:从Session到JWT的演进 一、Session共享让我头大2018年我们从单机扩展到多实例部署。用户反馈登录状态丢失——在A机器登录请求到了B机器就没登录了。原因是Session存在JVM内存中每个实例的Session是独立的。我们尝试了Session复制Tomcat Cluster但Session序列化和网络传输的开销太大。后来又用了Spring Session Redis虽然解决了共享问题但每次请求都要访问Redis增加了延迟。最终我们切换到了JWT彻底告别了Session。二、方案对比┌─────────────────────────────────────────────────────────────────┐ │ 会话管理方案对比 │ │ │ │ 方案 │ 状态 │ 性能 │ 扩展性 │ 适用场景 │ │ ───────────────────────────────────────────────────────────── │ │ 本地Session │ 有状态 │ 高 │ 差 │ 单机 │ │ Session复制 │ 有状态 │ 差 │ 差 │ 小集群 │ │ Spring Session │ 有状态 │ 中 │ 中 │ 传统Web │ │ JWT │ 无状态 │ 高 │ 好 │ 微服务/移动端 │ │ TokenRedis │ 半无状态│ 中 │ 好 │ 需要主动失效 │ │ │ └──────────────────────────────────────────────────────────────────┘三、JWT实现3.1 Token生成与验证/** * JWT工具类 */ComponentSlf4jpublicclassJwtTokenProvider{Value(${jwt.secret})privateStringsecret;Value(${jwt.access-token-expiration:7200})privatelongaccessTokenExpiration;Value(${jwt.refresh-token-expiration:604800})privatelongrefreshTokenExpiration;/** * 生成Access Token */publicStringgenerateAccessToken(UserDetailsuserDetails){MapString,ObjectclaimsnewHashMap();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{ClaimsclaimsJwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();returnJwtClaims.builder().userId(claims.get(userId,Long.class)).username(claims.getSubject()).roles((ListString)claims.get(roles)).expiration(claims.getExpiration()).build();}catch(ExpiredJwtExceptione){thrownewJwtTokenExpiredException(Token已过期);}catch(JwtExceptione){thrownewJwtTokenInvalidException(Token无效);}}}3.2 登录与Token刷新/** * 认证服务 */ServiceSlf4jpublicclassAuthService{AutowiredprivateJwtTokenProvidertokenProvider;AutowiredprivateStringRedisTemplateredisTemplate;/** * 登录 */publicLoginResultlogin(LoginRequestrequest){// 验证用户名密码UserDetailsuserauthenticate(request.getUsername(),request.getPassword());// 生成TokenStringaccessTokentokenProvider.generateAccessToken(user);StringrefreshTokentokenProvider.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){JwtClaimsclaimstokenProvider.validateToken(refreshToken);// 验证Refresh Token是否在Redis中StringstoredredisTemplate.opsForValue().get(refresh_token:claims.getUserId());if(!refreshToken.equals(stored)){thrownewJwtTokenInvalidException(Refresh Token无效);}// 生成新的Access TokenUserDetailsuserloadUser(claims.getUserId());StringnewAccessTokentokenProvider.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);}}四、踩坑实录坑1JWT无法主动失效用户改了密码但旧Token还能用。解决维护Token黑名单Redis或者缩短Access Token有效期Refresh Token轮换。坑2JWT太大JWT Payload塞了太多数据Token超过4KB超过Header限制。解决JWT只存必要信息userId、username其他信息按需查询。坑3Token存储不安全前端把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最佳实践JWT只存必要信息Access Token短有效期2小时Refresh Token长有效期7天Token安全存储做好Token刷新和失效血的教训JWT不是万能的。无状态是优势也是劣势选择方案前先想清楚你的业务需要什么。思考题你的系统用了什么会话管理方案个人观点仅供参考