资讯动态

JeecgBoot 3.2.0在线用户异常排查:token刷新与Redis存储策略优化

发布时间:2026/10/14 21:09:06 来源:尧图企业网站定制
JeecgBoot 3.2.0 用了一段时间后很多团队都会撞上同一个尴尬场景系统监控里的在线用户列表同一账号翻来覆去出现好几条记录IP、登录时间还各不相同管理员在列表里点强制下线前端明明提示已退出用户刷新一下页面又恢复了甚至有人改了密码旧账号还能通过一段时间内未被踢掉的 token 继续操作。这不是偶发毛病而是 token 刷新机制、Redis 存储结构、在线用户展示逻辑三者没有对齐造成的典型连锁反应。我在这篇文章里把整个链路翻出来讲清楚token 是怎么签发、续期、存储和展示的问题到底出在哪个环节以及用什么样的代码改动才能真正收口。适合正在做 JeecgBoot 二次开发、或者刚升级到 3.2.0 后被在线用户模块坑过的同学参考。1. 问题现象与影响范围先对齐1.1 三个最容易感知的异常现场第一个是同人多记录。用户只用一台电脑、一个浏览器登录后台在线用户列表里却能出现两条甚至更多条一模一样的用户名记录。每条记录的登录时间、IP 可能都不相同看起来就像被异地登录了一样。第二个是强制下线失效。管理员在在线用户列表选中某条记录执行下线返回成功但这个用户实际仍然可以正常访问接口。原因往往是列表里展示的 token 和用户手里实际持有的 token 已经不是同一个了下线删的是旧 token用户用的却是新 token。第三个是登录时间/IP 错乱。列表里显示的登录时间停留在第一次登录的时间刷新 token 之后没有再更新或者用户换了网络出口 IP 后列表里记录的还是旧 IP。对于需要做安全审计的系统来说这个数据失真比功能异常更危险。1.2 影响范围往往超出预期在线用户列表在 JeecgBoot 里不是一个纯展示页面它背后牵扯三块业务多端登录限制的判断逻辑、管理员的强制下线操作、登录审计与活跃用户统计。列表数据一旦不准单点登录会误踢正常会话安全审计报表会漏人运维在看谁在线的结论也会被误导。比如开启了同一账号最多允许 N 个会话在线用户正常使用过程中 token 刷新了几次系统会把刷新产生的 token 当作新的登录端从而踢掉用户在其他设备上真正在用的会话。这种问题排查起来最难受因为它不是必现的往往在用户连续操作超过一定时间后才触发等接到反馈时 Redis 里的 key 已经被 TTL 清理了一部分现场很难还原。2. 先看底层token 的签发、存储与在线展示逻辑2.1 登录成功之后 Redis 里发生了什么JeecgBoot 3.2.0 的登录逻辑走的是 JWT Redis 缓存的方式。用户输入用户名密码校验通过后后端生成一个 JWT 格式的 token同时把这个 token 写入 Redis。写入时用的 key 是一个固定的业务前缀加上 token 字符串本身也就是prefix_user_token_作为前缀拼接完整 token。这段逻辑藏在登录服务内部表面上看就是一个生成 token 调 redisUtil.set但有几个关键细节影响后续排查token 的超时时间、Redis key 的过期时间、value 里存了什么。很多团队改配置时只改了前端的登录过期提示忽略了 Redis key 的 TTL结果就是 token 还在有效期内Redis 里的记录却先过期了在线状态直接丢了一半。value 的存储内容在 3.2.0 里并不是标准化的 JSON 会话对象很多版本直接存 token 字符串本身或者只存用户名。这意味着在线用户列表拿不到用户登录时的 IP、登录时间、最后活跃时间这些完整信息只能靠 JWT 本身解析出的用户名去反查用户表再补几个基础字段。数据结构上的薄弱给后面几次改动埋了隐患。2.2 在线用户列表的数据来源是怎么拼出来的在线用户页面的后端接口最常见的实现方式就是通过 Redis 的 keys 或者 scan 操作把所有以prefix_user_token_开头的 key 拉出来再逐个解析 key 里的 token从 token 里取出用户名最后组装成一个用户在线列表。这里的关键点在于页面上的一条记录本质上对应的是 Redis 里的一个 key而不是一个真实的自然人。用户只要持有两个有效 token列表里就有两条记录持有三个就有三条。列表接口本身根本不去判断这些 token 是否属于同一个人。按 key 数量展示在线人数的逻辑在小规模内部系统里问题不大一旦用户基数上来、token 有长 TTL再叠加刷新产生的碎片 key列表数据就会肉眼可见地膨胀。你在监控页面上看到在线用户 500 人实际活跃的人可能只有 200 多剩下 300 条全是过期未清理的 token 残留。2.3 刷新 token 的触发手段有哪些JeecgBoot 3.2.0 里面存在多种 token 刷新路径这是问题最容易扩散的原因之一。第一种是后端自动续期。每次请求经过鉴权过滤器时系统校验 token 正常且即将过期或者已过期但还在 Redis 宽限期内就自动生成一个新 token 写进 Redis并通过响应头或响应体把新 token 返回给前端。第二种是手动刷新接口。前端在请求拦截器里捕获到某种需要刷新的信号时主动调用后端的刷新接口换取新 token再拿着新 token 重放之前的业务请求。第三种是重新登录。用户登录态完全失效后走一遍登录流程生成一个全新的 token。前两种刷新路径有个共同特征生成新 token 时系统只做了写入新 key几乎没有做删除旧 key。旧 key 会一直存在 Redis 里直到它的 TTL 走完。而在线用户列表只看 key 数量这就在两个模块之间形成了一个数据黑洞。3. 问题根因四条链路里的断裂点3.1 刷新时新旧 token 并存Redis 里淤积碎片把刷新 token 的代码翻出来看典型逻辑长这样// 伪代码刷新token时只写入新key没有删除旧key String newToken JwtUtil.sign(username, password); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN newToken, username, expireTime); // 缺少: redisUtil.del(CommonConstant.PREFIX_USER_TOKEN oldToken)用户持有一个 token 正常访问每次刷新都会在 Redis 里新增一个 key。刷新次数越多旧 key 残留越多在线列表就越臃肿。TTL 设得越长这个问题被掩盖的时间越久TTL 设得太短又会造成 token 还没失效但 Redis 记录已经没了用户被迫重新登录。真正不合理的不是 TTL 本身而是刷新动作没有执行旧失效、新生效的语义。一个会话的生命周期里同时只应该有一个有效 token旧 token 在生成新 token 的那一刻就应该被标记失效。3.2 在线列表按条数展示而不是按用户展示接口在组装在线用户集合时没有对同一个用户 ID 的 token 做去重聚合。Redis 里有多少个 key列表就有多少行。这里不只是一个展示美观问题。列表里的字段如果从不同 key 上解析同一用户的多条记录可能带有不同的 IP、不同的登录时间管理员根本无法分辨哪一条才是真正活跃的会话。更麻烦的是前端的表格组件常常会把用户名作为排序或筛选维度当同一个人出现多行时管理员的每一次点击操作都面临选错行的风险。3.3 强制下线依赖精确 token 匹配JeecgBoot 在线用户列表里的下线按钮通常是把这一行对应的 token 在 Redis 里删掉让后续请求校验失败。这个逻辑在列表展示的 token 就是用户当前实际持有 token的前提下是有效的。问题在于token 历经刷新之后列表展示的可能是旧 token用户手里拿的是新 token。这时候管理员点下线删除的是旧 key用户的下一个请求带着新 token 过来校验依然通过看起来就是点下线无效。用户无感知管理员很崩溃。还有一种情况是用户在多个设备上登录列表里同一个人的多条记录分别是不同设备的 token。管理员只想让用户在某台设备上退出但列表里多行数据对应的设备关系又没有标识操作时很容易误删其他设备的会话。3.4 刷新时没有回写会话信息在线用户列表里展示的登录时间和登录 IP如果是从登录时写入的数据取的那么 token 刷新之后这部分信息天然是旧的。更严谨的设计应该把每次刷新动作当作会话活跃事件刷新时更新该会话的 IP 和最后活跃时间。实际情况是刷新流程只关心 token 有没有更新没有人去更新会话元数据。久而久之在线列表的审计价值不断下降运营同学对着数据根本说不出用户到底什么时候登录、最后一个请求从哪儿来。4. 解决方案四个改动逐步把问题收口4.1 改动一刷新 token 时执行先写新、再删旧最直接有效的一步是让刷新动作具备旧 token 立即失效的语义。推荐顺序是先写入新 key再删除旧 key。// 伪代码刷新token时完成新旧替换 String oldToken request.getHeader(X-Access-Token); String username JwtUtil.getUsername(oldToken); // 1. 基于用户名或旧token生成新token String newToken JwtUtil.sign(username, oldToken); // 2. 先写新key确保写入成功后旧key才删除 redisUtil.set(CommonConstant.PREFIX_USER_TOKEN newToken, username, expireTime); // 3. 删除旧key redisUtil.del(CommonConstant.PREFIX_USER_TOKEN oldToken); // 4. 新token通过响应头返回 response.setHeader(X-Access-Token, newToken);先写新后删旧是为了避免中间一个时间窗口里请求带着旧 token 过来却找不到 Redis 记录导致被误判为未登录。如果先删旧再写新刷新接口自身在写新 token 之前还有一小段间隔极端情况下可能产生 401 抖动。这里有一个并发风险同一个用户发出多个并发请求每个请求都触发刷新就可能生成多个新 token产生新的碎片。规避手段是在刷新逻辑里加一个按用户名粒度的锁或者用 Redis 的原子操作保证同一个用户同时只有一个刷新任务在执行。如果并发量不大用 JVM 内置的 ConcurrentHashMap 加锁也能凑合但集群部署时就必须依赖 Redis 分布式锁或者严格的旧 key 对比删除策略了。4.2 改动二在线列表按用户维度聚合即使刷新逻辑做了新旧替换老数据在 Redis 里可能还有遗留。在线用户列表接口需要加一层聚合逻辑按用户 ID 分组每个用户只展示最新一条会话记录。// 伪代码在线用户列表按userId去重 ListTokenInfo tokenList loadTokenInfosFromRedis(); MapString, TokenInfo latestByUser new HashMap(); for (TokenInfo info : tokenList) { TokenInfo old latestByUser.get(info.getUserId()); if (old null || info.getRefreshTime().isAfter(old.getRefreshTime())) { latestByUser.put(info.getUserId(), info); } } return latestByUser.values();聚合时选择保留哪一条的标准建议用最后活跃时间而不是登录时间。因为最后活跃时间才反映用户当前正在使用的会话登录时间早的会话可能是用户长期没关机挂着的。如果产品上确实需要展示同一用户的多个在线端手机、平板、PC那列表接口要增加设备标识字段否则多条记录无法区分。再进一步就是引入会话 ID 的概念把一条会话作为第一公民token 只是会话内部轮换的凭证。4.3 改动三强制下线改成按用户清场管理员的诉求通常是让这个用户下线而不是删除某一串 token。下线接口应该接受一个用户 ID 或用户名然后找到该用户关联的所有有效 token一次性清掉。// 伪代码按用户id清理所有在线token SetString userTokenKeys getKeysByUser(userId); for (String key : userTokenKeys) { redisUtil.del(key); }获取用户关联 token keys 的方式可以在 Redis 里额外维护一个user_sessions:前缀的集合每次登录或刷新时把 user - token 的关系写进去下线时读取集合逐一删除也可以直接在登录时把 userId 拼进 key比如prefix_user_token_ userId _ token这样扫描范围更小、删除更精准。按用户清场还有一个作用解决刷新导致的踢不掉问题。不管用户手里换过多少个 token只要生成新 token 时都把关系记录在案下线时全部清理用户就再也发不出有效请求。4.4 改动四会话信息结构化刷新时同步更新最后一步是把 Redis 里 token key 对应的 value 从简单的 token 字符串改为结构化的会话对象包含 userId、用户名、登录 IP、登录时间、最后活跃时间、刷新次数等字段。登录时写入初始会话刷新时只更新最后活跃时间和 IP。// 伪代码写入结构化会话对象 SessionInfo info new SessionInfo(); info.setUserId(userId); info.setUsername(username); info.setLoginIp(ip); info.setLoginTime(new Date()); info.setLastActiveTime(new Date()); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN token, JSON.toJSONString(info), expireTime);在线列表接口拿到 value 后直接反序列化不需要反查用户表响应速度更快展示字段也更完整。登录 IP 在刷新时更新审计数据才能反映用户真实的网络出口变化。这一步改动量稍大但收益也最实在在线用户列表终于变成了会话列表而不是token 列表。4.5 方案对比与选型建议改动项解决的核心问题改动量风险点适用场景刷新时删旧token碎片key淤积、同人多记录小并发刷新可能误删新token所有使用3.2.0的项目建议立刻做在线列表按用户聚合列表展示虚高、操作选错行小多端登录场景被合并内部管理系统、没有强多设备诉求时下线按用户清场强制下线失效中可能误杀用户所有设备管理员诉求是让用户下线而非踢设备会话数据结构化登录时间/IP不准确中需要兼容旧数据有安全审计、运维展示诉求的项目我建议至少完成改动一和改动三。改动一是从源头减少碎片改动三是保证管理员的强制操作真实可控。改动二更多是缓解展示层症状改动四是数据基础升级可以按团队节奏排期。5. 现场排查实操与验证清单5.1 四步定位问题如果你已经在生产环境撞上这个问题不要急着改代码先按下面几步确认根因再动手。第一步确认 Redis 里 token key 的数量。用客户端连上 Redis执行查询redis-cli keys prefix_user_token_*看返回的 key 数量再对比在线用户页面的记录数。如果 key 数量远大于页面展示数说明页面做了部分过滤如果两者几乎一致说明页面是纯 key 扫描。第二步复现一次 token 刷新操作。打开一个已登录用户的管理页面等 token 接近过期时间触发任意一个接口请求然后立刻再执行一次 keys 查询观察prefix_user_token_前缀下的 key 数量变化。如果原来一个 key刷新后变成两个而旧的还在说明刷新逻辑确实没有删旧 key。第三步检查刷新前后在线用户列表接口的返回 JSON。找到同一个用户名对应的 records 条数变化。这一步能直接确认列表是否按 key 数量展示以及刷新动作是否导致同用户多记录。第四步模拟强制下线。在线列表里定位到刚才那个用户选择其中一条记录执行下线然后用同一个账号再请求一次受保护的接口。如果接口仍然返回 200说明前端持有的 token 并没有被删除基本坐实了旧 token 被删、新 token 还在的问题。5.2 常用辅助命令与注意事项排查过程中这几个 Redis 命令经常用到# 查看某个token key的剩余过期时间 redis-cli ttl prefix_user_token_xxx # 查看某个token key里存了什么 redis-cli get prefix_user_token_xxx # 按scan方式遍历避免keys阻塞 redis-cli --scan --pattern prefix_user_token_*生产环境执行keys要非常克制尤其在 key 数量大的时候keys *这类操作会阻塞 Redis 线程。用--scan --pattern更加安全。如果是在 Java 代码里做同类扫描优先使用 Spring Data Redis 的 scan 方法而不是redisUtil.keys。5.3 配置项排查清单代码层面的问题找完后还要把配置过一遍确认 token 有效期的配置项比如超时时间是否与 Redis key 的 TTL 匹配一般建议 Redis TTL 略大于 JWT 过期时间留出刷新时间窗口。检查是否开启了多端登录限制以及最大同时在线数的配置。刷新 token 在没有删旧 key 的情况下会被误判为新的登录端从而挤掉其他设备。检查鉴权过滤器的 URL 排除规则确认刷新 token 的接口不是被放行后遗漏了清理逻辑。如果系统部署在 Redis 集群或者分片环境下注意不同节点上的 key 扫描结果是否完整避免漏掉某个节点的在线会话。6. 避坑记录与个人建议排查这类问题的过程中有几个坑值得单独拿出来说。不要一上来就flushdb清空 Redis 验证效果。生产环境的 Redis 里通常还放着权限缓存、字典缓存、验证码缓存等业务数据一清全没用户会大面积掉线。真想验证就用前缀精确匹配只删prefix_user_token_相关的 key。刷新逻辑改动后要跟前端联调确认响应头里新 token 的传递方式。如果前端 Axios 拦截器只拦截了响应体里的 token没处理响应头新 token 不会真正生效刷新链路等于断了。我见过不少团队改完后端刷新逻辑前端频繁出现重复登录最后发现是响应的新 token 没有写进本地存储。多个浏览器标签页并存的场景也要提前考虑。A 标签页刷新了 tokenB 标签页还在用旧 token后续请求就会校验失败。解决思路一般是用 localStorage 加事件监听token 更新后通知其他标签页同步更新或者在服务端放宽 B 标签页场景下的校验容错。升级版本前建议先查一遍 3.2.0 相对 3.0 系列在登录鉴权这块有什么调整重点看 token 前缀常量、刷新接口路径、Redis key 结构有没有变化。很多从旧版本升上来的项目Redis 里残留着旧前缀的 key新代码认不出在线列表自然对不上。最后再分享一个经验值token 的 TTL 不要只拍脑袋定一个数。可以参考系统的真实活跃时长先统计一下日常用户连续操作的最长间隔再在这个基础上加 20% 余量作为 Redis key 的 TTL。刷新机制正常的前提下用户长时间不操作才需要重新登录频繁操作的用户应当在每次刷新时得到续期而不是隔几个小时就被迫退出一次。这样配置出来的在线状态才贴近真实使用体验。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑