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

Spring Security WebFlux:WebSession中SecurityContext持久化失败问题

Spring Security WebFlux自定义登录后后续请求无法获取SecurityContext问题排查

问题现象

从日志可见,登录时SecurityContext已成功存入WebSession,但后续请求却无法找到对应上下文:

WebSessionServerSecurityContextRepository : Saved SecurityContext 'SecurityContextImpl [...], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[USER]]]' in WebSession : 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@472c317e'

a.DelegatingReactiveAuthorizationManager : Checking authorization on '/users/userAccountInfo' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager@555fdfaf

WebSessionServerSecurityContextRepository : No SecurityContext found in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@e35c0ee'

现有代码配置

1. WebFlux Security配置类

@Configuration  
@EnableWebFluxSecurity  
public class SecurityConfiguration {

@Bean  
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {  
   http  
         .csrf().disable()  
         .securityContextRepository(securityContextRepository())
         .authorizeExchange(exchanges -> exchanges  
               .pathMatchers("/auth/**").permitAll()  
               .anyExchange().authenticated()  
         );  
   return http.build();  
}  

@Bean  
public WebSessionServerSecurityContextRepository securityContextRepository() {  
   return new WebSessionServerSecurityContextRepository();  
}

@Bean
public ReactiveUserDetailsService userDetailsService(AccountService accountService) {
   return accountService;
}

@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
   UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);
   authenticationManager.setPasswordEncoder(passwordEncoder);
   return authenticationManager;
}
}

2. 自定义登录方法

public Mono<Void> loginUser(ServerWebExchange exchange,  
               Authentication authRequest,  
               ReactiveAuthenticationManager  reactiveAuthenticationManager) {  
return reactiveAuthenticationManager.authenticate(authRequest)  
      .flatMap(authResponse -> {  
         SecurityContext securityContext = new SecurityContextImpl(authResponse);  
         return securityContextRepository.save(exchange, securityContext);  
      })  
      .onErrorResume(e -> Mono.error(new InvalidCredentialsException("Invalid credentials: " + e)));  
}

3. 自定义WebSessionServerSecurityContextRepository的save方法

@Override  
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {  
    return exchange.getSession().doOnNext((session) -> {  
       if (context == null) {  
          session.getAttributes().remove(this.springSecurityContextAttrName);  
          logger.debug(LogMessage.format("Removed SecurityContext stored in WebSession: '%s'", session));  
       }  
       else {  
          session.getAttributes().put(this.springSecurityContextAttrName, context);  
          logger.debug(LogMessage.format("Saved SecurityContext '%s' in WebSession: '%s'", context, session));  
       }  
    }).flatMap(WebSession::changeSessionId);  
}

用户曾尝试添加会话并发管理配置,但未解决问题:

@Bean
  SecurityWebFilterChain filterChain(ServerHttpSecurity http, ReactiveSessionRegistry sessionRegistry) {
      http
          // ...
          .sessionManagement((sessionManagement) -> sessionManagement
              .concurrentSessions((concurrentSessions) -> concurrentSessions
                  .maxSessions(1)
                  .maxSessionsPreventsLogin(true)
                  .sessionRegistry(sessionRegistry)
              )
          );
      return http.build();
  }

问题原因分析

  1. 会话ID变更导致上下文丢失:自定义save方法中调用了flatMap(WebSession::changeSessionId),该操作会生成新会话ID,但登录请求响应未将新ID返回给客户端,后续请求使用旧会话ID自然无法找到对应SecurityContext。
  2. 自定义save方法逻辑冗余:默认WebSessionServerSecurityContextRepository的save方法不会主动调用changeSessionId,会话固定防护Spring Security默认会在认证成功后自动处理,手动调用反而引发问题。

解决方案

方案一:移除自定义save方法中的changeSessionId调用

若无需手动处理会话固定防护,直接使用默认WebSessionServerSecurityContextRepository实现,无需重写save方法:

@Bean  
public WebSessionServerSecurityContextRepository securityContextRepository() {  
   return new WebSessionServerSecurityContextRepository();  
}

若必须自定义save方法,删除changeSessionId调用:

@Override  
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {  
    return exchange.getSession().doOnNext((session) -> {  
       if (context == null) {  
          session.getAttributes().remove(this.springSecurityContextAttrName);  
          logger.debug(LogMessage.format("Removed SecurityContext stored in WebSession: '%s'", session));  
       }  
       else {  
          session.getAttributes().put(this.springSecurityContextAttrName, context);  
          logger.debug(LogMessage.format("Saved SecurityContext '%s' in WebSession: '%s'", context, session));  
       }  
    }).then(); // 替换flatMap(WebSession::changeSessionId)为then()
}

方案二:使用Spring Security默认认证流程替代自定义登录

尽量避免手动处理SecurityContext存储,使用框架提供的默认登录端点或通过ServerAuthenticationSuccessHandler处理认证成功逻辑,减少手动操作错误:

http.formLogin(form -> form
    .authenticationSuccessHandler((webFilterExchange, authentication) -> {
        // 自定义成功逻辑
        return Mono.empty();
    })
);

会话仓库选择建议

  • 开发环境使用InMemoryWebSessionStore无问题,但生产环境建议使用分布式会话仓库(如Redis)解决多实例会话共享问题:
@Bean
public ReactiveSessionRepository<? extends WebSession> reactiveSessionRepository(RedisConnectionFactory connectionFactory) {
    return new RedisWebSessionRepository(connectionFactory);
}

注:当前问题并非会话仓库导致,核心是会话ID变更后客户端未同步新ID的问题。

内容的提问来源于stack exchange,提问作者BLCK

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 15:15:14