Spring Security获取在线用户数异常:每次请求生成新Token致计数错误
问题:无状态JWT应用中在线用户数统计持续增长的异常
背景与问题
需要统计当前应用的在线用户数量,但应用采用无状态JWT认证,每次用户发起请求时都会生成新Token(React端同步替换旧Token)。尝试用SessionRegistry实现统计,但因每次请求都会触发新Token生成,导致统计的在线用户数持续增长,无法反映真实情况。
相关代码
获取会话统计接口
@Autowired private SessionRegistry sessionRegistry; @GetMapping public ResponseEntity<Response<Integer>> getSessions(){ List<String> userDetailsList = sessionRegistry.getAllPrincipals().stream() .filter(u -> !sessionRegistry.getAllSessions(u, false).isEmpty()) .map(Object::toString) .collect(Collectors.toList()); int sessions = userDetailsList.size(); //MySessionListener.getActiveSessions(); Response<Integer> response = new Response<>(); response.setData(sessions); return ResponseEntity.ok(response); }
Spring Security配置类
@Bean public SessionRegistry sessionRegistry() { return new SessionRegistryImpl(); } @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic().and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .cors() .and() .authorizeRequests() .antMatchers( "/v3/api-docs/**", "/swagger-ui/**" ).permitAll() .anyRequest().authenticated() .and().csrf().disable(); http.headers().frameOptions().disable(); http.addFilterBefore( authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class ); http .headers().cacheControl(); http .sessionManagement() .maximumSessions(1).sessionRegistry(sessionRegistry()); }
请求拦截器中的Token刷新逻辑
if (jwtTokenUtil.canTokenBeRefreshed(authToken)) { authToken = jwtTokenUtil.refreshToken(authToken); } request.setAttribute("x-auth", authToken);
环境补充
应用部署在WildFly 18服务器,请求由Apache反向代理处理。
问题根源与解决方案
核心原因
- 无状态会话与SessionRegistry的冲突:你配置了
SessionCreationPolicy.STATELESS,说明应用不依赖HTTP Session,但同时又启用了maximumSessions(1).sessionRegistry(sessionRegistry()),这会导致Spring Security每次请求都注册一个新的虚拟会话到SessionRegistry,且无状态模式下没有会话过期清理机制,旧会话持续累积。 - Token频繁刷新的加剧作用:每次请求都刷新Token,等同于触发新的认证流程,进一步增加了无效会话的注册量。
解决步骤
1. 废弃SessionRegistry,改用JWT专属统计方案
无状态应用下,SessionRegistry依赖HTTP Session的机制完全不适用,建议基于JWT特性结合缓存实现统计:
- 维护活跃Token缓存:
- 用户登录时,生成JWT并存入Redis(或其他缓存),设置与Token一致的过期时间。
- 刷新Token时,删除旧Token缓存,存入新Token。
- 用户主动登出时,直接删除对应缓存条目。
- 统计时对缓存中的Token按用户去重(若业务限制单用户单设备,刷新时直接覆盖缓存即可)。
2. 清理无效的SessionRegistry配置
移除sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry())这段配置,避免不必要的会话注册逻辑。
3. 示例代码(基于Redis实现)
登录时存储Token
@Autowired private StringRedisTemplate redisTemplate; public String login(String username, String password) { // 用户验证逻辑省略 String token = jwtTokenUtil.generateToken(userDetails); // 以用户名为key存储当前有效Token,设置过期时间 redisTemplate.opsForValue().set("online_user:" + username, token, jwtTokenUtil.getExpiration(), TimeUnit.MILLISECONDS); return token; }
刷新Token时更新缓存
public String refreshToken(String oldToken) { String username = jwtTokenUtil.getUsernameFromToken(oldToken); String newToken = jwtTokenUtil.refreshToken(oldToken); // 覆盖旧Token缓存,更新过期时间 redisTemplate.opsForValue().set("online_user:" + username, newToken, jwtTokenUtil.getExpiration(), TimeUnit.MILLISECONDS); return newToken; }
统计在线用户数
public ResponseEntity<Response<Integer>> getOnlineUsers() { // 匹配所有用户缓存key,统计数量 Set<String> userKeys = redisTemplate.keys("online_user:*"); int count = userKeys != null ? userKeys.size() : 0; Response<Integer> response = new Response<>(); response.setData(count); return ResponseEntity.ok(response); }
4. 自动处理离线场景
利用Redis的自动过期机制,Token过期后对应缓存条目会自动删除,无需手动清理;用户主动登出时直接删除缓存即可。
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

