Session 共享不是加个 Redis 就行:一次登录态串号事故,和 4 种方案的真实取舍

发布时间:2026/8/1 19:26:42
Session 共享不是加个 Redis 就行:一次登录态串号事故,和 4 种方案的真实取舍 title: Session 共享不是加个 Redis 就行一次登录态串号事故和 4 种方案的真实取舍tags: Session, Redis, 分布式, Spring Session, JWTdescription: 从一次用户 A 看到用户 B 数据的串号事故出发拆解 Session 复制、Cookie、集中存储、JWT 四种共享方案的适用边界以及 Spring Session 落地的真实坑。去年我们线上出过一个特别诡异的工单用户投诉我明明登录的是自己的账号刷新一下却变成了别人的名字。一开始我们以为是前端 bug查了三天才定位到——是 Session 共享那一层出了岔子。这篇文章把四种 Session 共享方案拆开讲重点是我不希望你踩的那几个坑。先给不熟悉的兄弟补一句背景传统的HttpSession默认存在应用服务器本地内存里。单体时代没问题但一旦上了负载均衡、应用起了多个实例用户第一次请求打到机器 A 登录第二次请求被轮询到机器 BB 上根本没有这个 Session于是又让你重新登录甚至更糟——读到 B 上残留的别人 Session。方案一Session 复制tomcat cluster早期最粗暴的做法让多个 Tomcat 互相广播 Session。配置一下Cluster节点就能用。!-- tomcat server.xml 里开启 DeltaManager 做 Session 复制 -- Cluster classNameorg.apache.catalina.ha.tcp.SimpleTcpCluster Manager classNameorg.apache.catalina.ha.session.DeltaManager expireSessionsOnShutdownfalse notifyListenersOnReplicationtrue/ !-- 复制的其实是序列化后的 Session 对象节点越多网络广播量越大 -- /Cluster逐行看DeltaManager只同步增量变更比全量广播好一点但本质还是把每个 Session 序列化后在节点间互发。我们的教训是当节点数超过 4 个、Session 里又塞了胖对象广播流量直接把内网带宽吃满。我不建议新项目用这个方案它和加机器是反向的——机器越多越慢而且 Session 里对象稍有不可序列化就直接报错。方案二客户端 Cookie 存状态把状态加密后直接写进 Cookie服务端无状态。好处是天然支持横向扩展坏处是体积受限Cookie 一般 4KB 上限且每次请求都带在头上。// 把少量登录信息签名后写 Cookie public void writeSessionCookie(HttpServletResponse resp, LoginUser user) { String payload user.getUserId() | user.getRole(); // 1. 用 HMAC 签名防止客户端篡改绝不只是 Base64 String signed payload . hmacSha256(payload, SECRET_KEY); Cookie c new Cookie(SESSION, Base64.getUrlEncoder().encodeToString(signed.getBytes())); c.setHttpOnly(true); // 2. 防 XSS 偷 Cookie c.setSecure(true); // 3. 只走 HTTPS c.setMaxAge(1800); // 4. 半小时过期和服务端逻辑对齐 resp.addCookie(c); }第 4 行的hmacSha256是重点Cookie 方案最大的雷是只编码不签名我见过有人直接 Base64 把 userId 塞进去前端把userId1001改成1002就串号了。签名 HttpOnlySecure这三件套缺一不可。这套方案适合登录态很薄的场景一旦要存一堆权限、购物车Cookie 装不下就得换。方案三集中存储Spring Session Redis这是现在最主流的方案spring-session-data-redis帮你把HttpSession透明地搬到 Redis业务代码几乎零改动。EnableRedisHttpSession(maxInactiveIntervalInSeconds 1800) Configuration public class SessionConfig { // 1. 默认用 Lettuce 连接 Redis注意连接池必须配否则高并发下拿不到连接 Bean public LettuceConnectionFactory connectionFactory() { LettucePoolingClientConfiguration pool LettucePoolingClientConfiguration.builder() .poolConfig(new GenericObjectPoolConfig() {{ setMaxTotal(200); setMaxIdle(50); setMinIdle(10); }}).build(); return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redis-1, 6379), pool); } } // 业务代码完全不用改继续用原生 HttpSession GetMapping(/cart) public Cart getCart(HttpSession session) { return (Cart) session.getAttribute(cart); // 实际读写都走 Redis }EnableRedisHttpSession是魔法入口它用一个SessionRepositoryFilter拦截请求把你代码里的HttpSession偷偷替换成 Redis -backed 的实现。第 9 行那个连接池配置是我们用血换来的——Spring Session 默认的连接工厂没有池化QPS 一上来RedisConnection直接被借光线程全阻塞在等连接上表现就是接口超时飙红。串号事故Redis 超时回退到本地撞了别人的 Session那次工单的根因就在这。我们的 Redis 连接池当时设的maxTotal20某天 Redis 因为一次大 key 删除卡了 2 秒连接池被瞬间借空。Spring Session 拿不到 Redis 连接时没有报错而是静默回退到了本地 Map 存 Session这是个隐藏行为文档里写得很隐晦。后果是用户登录在 A 实例创建了本地 Session负载均衡下一跳到了 B 实例B 也有自己的本地 Session 池。更致命的是我们当时为了省内存Session 的 key 用的是JSESSIONID而本地 Map 的 key 空间和 Redis 的 key 空间没有隔离。两个实例本地都生成了同名JSESSIONID的概率为 0但问题出在另一个地方——我们的 nginx 当时配了ip_hash失效请求乱跳而本地 Session 回退后getAttribute偶尔读到的是另一个用户上次残留的本地对象。修复三件事把 Redis 连接池调大到 200 并加监控给 Session 的本地回退明确关闭不允许无 Redis 就降级给 Spring Session 配置独立的redisNamespace避免 key 冲突。从此再没串过。方案四JWT 无状态令牌JWT 把用户信息 签名打包成一个 token服务端不存任何 Session靠签名校验真伪。特别适合前后端分离、跨域、移动端。// 用 jjwt 签发与校验注意别踩过期和刷新的坑 public String issueToken(LoginUser user) { return Jwts.builder() .setSubject(user.getUserId()) .claim(role, user.getRole()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1800_000)) // 30 分钟 .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256) .compact(); } public Claims parse(String token) { // 校验签名和过期失败直接抛异常不会解析出伪造的 payload return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .build().parseClaimsJws(token).getBody(); }JWT 最大的争议是无法主动吊销token 发出去后在过期前都有效你没法像 Redis Session 那样del掉它。我们的做法是access token 短时效15 分钟 refresh token 长时效存 Redis要踢人就删 Redis 里的 refresh token。这样既享受无状态又能控制强制下线。我的取舍判断新项目、前后端分离、要跨域/移动端直接上 JWT 短 token 方案。Session 共享的复杂度可以省掉。老 Spring MVC 项目、不想改业务代码Spring Session Redis但连接池和命名空间这两件事必须当天配好否则迟早出串号或超时。Session 里别塞胖对象我们把整个Authentication对象含权限树塞进 Redis一个 key 20KB千万级 Session 直接把 Redis 内存打爆。改成只存 userId权限按需查体积降到 200 字节。Cookie 方案只适合极薄登录态别拿它当通用 Session 用。Session 这层看着简单真出事就是 P0 级的用户数据泄露。我不建议为了简单就上 Session 复制也不建议为了时髦就无脑 JWT 却不处理吊销。选型先问自己三个问题要不要主动踢人登录态有多胖要不要跨域答案清晰了方案自然就出来了。思考题如果你的 JWT 私钥泄露了你会怎么止血是立刻换 key 让所有老 token 失效误伤正常用户还是引入黑名单又回到了要存状态这两难你怎么解