You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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反向代理处理。


问题根源与解决方案

核心原因

  1. 无状态会话与SessionRegistry的冲突:你配置了SessionCreationPolicy.STATELESS,说明应用不依赖HTTP Session,但同时又启用了maximumSessions(1).sessionRegistry(sessionRegistry()),这会导致Spring Security每次请求都注册一个新的虚拟会话到SessionRegistry,且无状态模式下没有会话过期清理机制,旧会话持续累积。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 07:16:22