Spring WebFlux WebTestClient单元测试401及Principal空值问题
问题根因
你遇到的401和Principal为空问题,核心是三个认知偏差:
@WebFluxTest是Web层切片测试,默认不会加载你生产环境自定义的JWT解析、资源服务器鉴权等全量安全配置,你塞真实/测试token都会被默认安全拦截器拦截,直接返回401。- WebFlux是响应式技术栈,安全上下文不存于
SecurityContextHolder的ThreadLocal中,而是基于Reactor上下文传递,你之前手动mock ThreadLocal里的安全上下文完全不会被响应式链路读取,自然无效。 - 直接排除
ReactiveSecurityAutoConfiguration会彻底移除整个安全过滤器链,请求不会注入任何Principal对象,Controller拿到的值必然是null。
可行解决方案
按以下步骤修改即可同时解决401问题、注入非空Principal:
- 确保测试依赖中包含
spring-security-test,该依赖随Spring Boot WebFlux Test Starter默认引入,若缺失则补充与当前Spring Security版本一致的依赖即可。 - 移除
@WebFluxTest注解中excludeAutoConfiguration相关配置,不要禁用响应式安全自动配置。 - 在测试类中添加测试专用安全配置,放开所有请求的鉴权拦截,同时保留安全上下文注入能力:
@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(); } }
- 在测试类上通过
@Import(TestSecurityConfig.class)导入上述配置。 - 测试方法中构造符合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
相关产品推荐
相关产品推荐

