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

Spring WebFlux WebTestClient单元测试401及Principal空值问题

问题根因

你遇到的401和Principal为空问题,核心是三个认知偏差:

  • @WebFluxTest是Web层切片测试,默认不会加载你生产环境自定义的JWT解析、资源服务器鉴权等全量安全配置,你塞真实/测试token都会被默认安全拦截器拦截,直接返回401。
  • WebFlux是响应式技术栈,安全上下文不存于SecurityContextHolder的ThreadLocal中,而是基于Reactor上下文传递,你之前手动mock ThreadLocal里的安全上下文完全不会被响应式链路读取,自然无效。
  • 直接排除ReactiveSecurityAutoConfiguration会彻底移除整个安全过滤器链,请求不会注入任何Principal对象,Controller拿到的值必然是null。
可行解决方案

按以下步骤修改即可同时解决401问题、注入非空Principal:

  1. 确保测试依赖中包含spring-security-test,该依赖随Spring Boot WebFlux Test Starter默认引入,若缺失则补充与当前Spring Security版本一致的依赖即可。
  2. 移除@WebFluxTest注解中excludeAutoConfiguration相关配置,不要禁用响应式安全自动配置。
  3. 在测试类中添加测试专用安全配置,放开所有请求的鉴权拦截,同时保留安全上下文注入能力:
@TestConfiguration
public class TestSecurityConfig {
    @Bean
    public SecurityWebFilterChain testSecurityFilterChain(ServerHttpSecurity http) {
        http.csrf(ServerHttpSecurity.CsrfSpec::disable)
                .authorizeExchange(exchange -> exchange.anyExchange().permitAll())
                .securityContextRepository(NoOpServerSecurityContextRepository.getInstance());
        return http.build();
    }
}
  1. 在测试类上通过@Import(TestSecurityConfig.class)导入上述配置。
  2. 测试方法中构造符合Controller取值逻辑的mock认证对象,通过Spring Security Test提供的响应式Mock能力注入到WebTestClient中,不要手动往ThreadLocal塞上下文、也不要手动传Bearer Token:
@Test
public void test_success() {
    // 构造Controller需要的userInfo结构,与生产环境JWT解析后的结构保持一致即可
    Map<String, String> mockUserInfo = new HashMap<>();
    mockUserInfo.put("userId", "testUser");
    // 构造认证对象,权限、凭证字段测试场景可随意赋值
    UsernamePasswordAuthenticationToken mockAuth = new UsernamePasswordAuthenticationToken(
            "testUser",
            "testPwd",
            Collections.emptyList()
    );
    // 把mock的userInfo塞入details,对应Controller中((Authentication)principal).getDetails()的取值逻辑
    mockAuth.setDetails(mockUserInfo);

    TransactionDocumentResponse transactionDocumentResponse = new TransactionDocumentResponse();
    transactionDocumentResponse.setTransactionId(testTransactionIdBigInteger);
    ResponseEntity<TransactionDocumentResponse> response = ResponseEntity.of(Optional.of(transactionDocumentResponse));

    Mockito.doNothing().when(utility).checkForMissingToken(anyMap(), anyString());
    when(service.get(anyString(), anyMap())).thenAnswer(createAnswer(response));

    WebTestClient.BodySpec<JSONObject, ?> stringBodySpec = webTestClient
            // 核心:注入mock的认证信息到响应式安全上下文,注意导入reactive包下的authentication静态方法
            .mutateWith(org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.authentication(mockAuth))
            .get()
            .uri("/myController/{transactionId}", testTransactionId)
            .exchange()
            .expectStatus().isOk() // 此时不会再返回401
            .expectBody(JSONObject.class);

    assertEquals(123, stringBodySpec.returnResult().getResponseBody().get("transactionId"));
}
之前方案失效原因
  • 排除安全自动配置:安全过滤器链整体移除,没有组件负责往请求中注入Principal,必然空指针。
  • 传入Bearer Token:切片测试未加载生产环境的JWT校验逻辑,默认安全配置不识别传入的token,直接拦截返回401。
  • 手动Mock SecurityContextHolder:该类基于ThreadLocal存储上下文,仅对Servlet栈Spring MVC生效,WebFlux响应式链路不读取该存储位置的内容,完全无效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 20:48:21