Servlet环境下如何为响应式端点稳定传递Security Context?
我维护的Spring Boot应用基于MVC Servlet机制,其中部分API是返回Mono、Flux的响应式端点。项目采用基于Servlet的Spring Security,Security Context通过BearerTokenAuthenticationFilter设置。
目前遇到的问题:
- 普通Servlet控制器里Security Context正常可用
- 返回响应式Publisher(Mono/Flux)的控制器中,Security Context经常为空
已经试过的操作:
修改SecurityContextHolder的策略为MODE_INHERITABLETHREADLOCAL,代码如下:
@PostConstruct public void init() { SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); }
但这个方法只解决了部分场景,Security Context仍会偶发空值情况。
限制条件:项目依赖的遗留库只能在Servlet环境下运行,无法切换到响应式安全框架。
需求:有没有不需要逐方法传递参数,就能稳定维护Security Context的方案?
1. 通过自定义Filter将Security Context注入Reactor上下文
核心思路是:在请求进入时,把Servlet线程中的Security Context存入Reactor的Context(响应式框架的专属上下文,与ThreadLocal无关),这样响应式流无论切换到哪个线程,都能从Reactor Context中稳定获取安全上下文。
先实现自定义Filter:
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.GenericFilterBean; import reactor.core.publisher.Mono; import reactor.util.context.Context; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.IOException; public class SecurityContextReactorInjectFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 获取当前请求已认证的Security Context SecurityContext securityContext = SecurityContextHolder.getContext(); try { // 将Security Context注入到Reactor全局上下文 Mono.deferContextual(ctx -> { Context newCtx = ctx.put(SecurityContext.class, securityContext); return Mono.subscriberContext(newCtx); }).block(); // 继续执行后续Filter链 chain.doFilter(request, response); } finally { // 遵循Spring Security规范清理ThreadLocal SecurityContextHolder.clearContext(); } } }
在Spring Security配置中,将该Filter放在BearerTokenAuthenticationFilter之后(确保先完成认证再注入上下文):
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 注入Reactor上下文的Filter置于认证Filter之后 .addFilterAfter(new SecurityContextReactorInjectFilter(), BearerTokenAuthenticationFilter.class) // 保留原有安全配置 .authorizeRequests() .anyRequest().authenticated(); } }
2. 全局封装Reactor上下文的安全上下文获取方法
编写工具类统一从Reactor Context中获取Security Context,避免重复代码:
import org.springframework.security.core.context.SecurityContext; import reactor.core.publisher.Mono; public class ReactiveSecurityContextHolder { public static Mono<SecurityContext> getContext() { return Mono.deferContextual(ctx -> Mono.justOrEmpty(ctx.getOrDefault(SecurityContext.class, null)) ); } }
在响应式控制器中的使用示例:
@RestController @RequestMapping("/reactive") public class ReactiveController { @GetMapping("/current-user") public Mono<String> getCurrentUserName() { return ReactiveSecurityContextHolder.getContext() .map(securityCtx -> securityCtx.getAuthentication().getName()); } }
为什么MODE_INHERITABLETHREADLOCAL会偶发空值?
Reactor的线程池(比如并行流、异步线程切换时使用的线程)是预创建的,这些线程的父线程并非处理请求的Servlet线程。InheritableThreadLocal仅能传递线程创建时的父线程上下文,对预创建的线程池无效,因此切换到这类线程时就会出现Security Context为空的情况。而通过Reactor Context传递的方式与线程无关,能在整个响应式流中稳定获取上下文。
内容的提问来源于stack exchange,提问作者Mohendra Amatya

