Spring Security WebFlux:WebSession中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(); }
问题原因分析
- 会话ID变更导致上下文丢失:自定义
save方法中调用了flatMap(WebSession::changeSessionId),该操作会生成新会话ID,但登录请求响应未将新ID返回给客户端,后续请求使用旧会话ID自然无法找到对应SecurityContext。 - 自定义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

