news 2026/8/1 19:26:29

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Session 共享不是加个 Redis 就行:一次登录态串号事故,和 4 种方案的真实取舍

title: Session 共享不是加个 Redis 就行:一次登录态串号事故,和 4 种方案的真实取舍
tags: Session, Redis, 分布式, Spring Session, JWT
description: 从一次用户 A 看到用户 B 数据的串号事故出发,拆解 Session 复制、Cookie、集中存储、JWT 四种共享方案的适用边界,以及 Spring Session 落地的真实坑。


去年我们线上出过一个特别诡异的工单:用户投诉"我明明登录的是自己的账号,刷新一下却变成了别人的名字"。一开始我们以为是前端 bug,查了三天才定位到——是 Session 共享那一层出了岔子。这篇文章把四种 Session 共享方案拆开讲,重点是我不希望你踩的那几个坑。

先给不熟悉的兄弟补一句背景:传统的HttpSession默认存在应用服务器本地内存里。单体时代没问题,但一旦上了负载均衡、应用起了多个实例,用户第一次请求打到机器 A 登录,第二次请求被轮询到机器 B,B 上根本没有这个 Session,于是又让你重新登录,甚至更糟——读到 B 上残留的别人 Session。

方案一:Session 复制(tomcat cluster)

早期最粗暴的做法,让多个 Tomcat 互相广播 Session。配置一下<Cluster>节点就能用。

<!-- tomcat server.xml 里开启 DeltaManager 做 Session 复制 --> <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"> <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/> <!-- 复制的其实是序列化后的 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 塞进去,前端把userId=1001改成1002就串号了。签名 +HttpOnly+Secure这三件套缺一不可。这套方案适合"登录态很薄"的场景,一旦要存一堆权限、购物车,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 连接池当时设的maxTotal=20,某天 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 失效(误伤正常用户),还是引入黑名单(又回到了要存状态)?这两难你怎么解?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/1 19:17:42

免费版AI生成原型工具靠谱吗?深度实测优缺点与避坑指南

说到免费版AI生成原型工具到底靠不靠谱&#xff0c;我的结论很直接&#xff1a;个人练手、做低保真草图、内部头脑风暴&#xff0c;绝对靠谱&#xff1b;但如果是商用交付、做高保真交互原型、给开发当依据&#xff0c;那基本不靠谱。 这篇文章我就从自身使用经历出发&#xff…

作者头像 李华
网站建设 2026/8/1 19:09:53

ESP32-S3-Touch-LCD-4.3B开发板:从驱动到LVGUI的智能家居中控实战

1. 项目缘起&#xff1a;为什么是ESP32-S3-Touch-LCD-4.3B&#xff1f;最近在做一个智能家居中控的Demo&#xff0c;需要一块带触摸屏的开发板作为交互核心。市面上这类板子不少&#xff0c;从简单的Arduino TFT Shield到功能强大的树莓派加触摸屏&#xff0c;选择很多。但我的…

作者头像 李华
网站建设 2026/8/1 19:09:19

SubtitleEdit终极指南:免费开源字幕编辑器的完整使用教程

SubtitleEdit终极指南&#xff1a;免费开源字幕编辑器的完整使用教程 【免费下载链接】subtitleedit the subtitle editor :) 项目地址: https://gitcode.com/gh_mirrors/su/subtitleedit SubtitleEdit是一款功能强大的免费开源字幕编辑器&#xff0c;支持SRT、ASS、VTT…

作者头像 李华
网站建设 2026/8/1 19:06:29

解锁百度网盘macOS版全速下载:逆向工程实践指南

解锁百度网盘macOS版全速下载&#xff1a;逆向工程实践指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 百度网盘macOS版用户常面临下载速度限制的困…

作者头像 李华
网站建设 2026/8/1 19:02:57

Anaconda vs Miniconda:Python 环境管理

Anaconda vs Miniconda&#xff1a;Python 环境管理1. 选择 Anaconda or Miniconda1.1. Anaconda or Miniconda?1.2. GUI versus command line installer1.3. Cryptographic hash verification2. Managing Python2.1. Viewing a list of available Python versions2.2. Instal…

作者头像 李华