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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 02:18:20