如何可靠断言ReactiveSecurityContextHolder中的内容?
说明
本问题与之前的Project Reactor相关问题不同,本次聚焦Spring WebFlux Security场景。
认证提取过滤器实现
以下是一个简化的认证提取过滤器(仅提取认证信息,不做认证校验):
import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerHttpBasicAuthenticationConverter; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; public class BasicWebFilter implements WebFilter { ServerAuthenticationConverter authenticationConverter = new ServerHttpBasicAuthenticationConverter(); @SuppressWarnings("NullableProblems") @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return authenticationConverter.convert(exchange) .flatMap(authentication -> chain.filter(exchange) .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))); } }
原有测试代码(可正常运行)
以下测试代码能验证上述过滤器的功能:
package com.example.dynamicgateway.misc; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; public class GenericTest { @Test void test() { String username = "username", password = "password"; MockServerHttpRequest request = MockServerHttpRequest.get("/") .header(HttpHeaders.AUTHORIZATION, "basic " + HttpHeaders.encodeBasicAuth(username, password, null)) .build(); MockServerWebExchange exchange = MockServerWebExchange.builder(request).build(); WebFilterChain chainMock = mock(WebFilterChain.class); given(chainMock.filter(exchange)).willReturn(Mono.empty()); BasicWebFilter basicWebFilter = new BasicWebFilter(); StepVerifier.create(basicWebFilter.filter(exchange, chainMock)) .expectAccessibleContext() .assertThat(c -> StepVerifier.create(c.<Mono<SecurityContext>>get(SecurityContext.class)) .expectNextMatches(sc -> { Authentication authentication = sc.getAuthentication(); return authentication.getPrincipal().equals(username) && authentication.getCredentials().equals(password); }) .verifyComplete()) .then() .verifyComplete(); } }
测试的脆弱性:修改过滤器后测试失败
当给过滤器添加onErrorResume错误处理逻辑后,原有测试会失败:
修改后的过滤器代码:
@Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return authenticationConverter.convert(exchange) .flatMap(authentication -> chain.filter(exchange) .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))) .onErrorResume(Exception.class, t -> { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); }); }
失败原因:原有测试断言的是最下游操作符的上下文,但添加onErrorResume后,下游变成了不保留原始上下文的MonoOnErrorResume,导致无法获取到预期的SecurityContext。
问题
请问有哪些更可靠的策略来测试ReactiveSecurityContextHolder的内容?如果可能,希望保持真正的单元测试(无需初始化Spring上下文)。
解决方案
针对这个问题,有几种可靠的单元测试策略:
1. 在过滤器链中捕获上下文
不要直接断言过滤器返回的Mono的上下文,而是在模拟的WebFilterChain中获取并验证SecurityContext。这样不管过滤器后续添加什么操作符(比如onErrorResume),只要上下文能传递到链中,测试就能生效。
修改测试中的WebFilterChain模拟逻辑:
WebFilterChain chainMock = mock(WebFilterChain.class); given(chainMock.filter(exchange)) .willAnswer(invocation -> { // 从当前上下文获取SecurityContext并验证 return ReactiveSecurityContextHolder.getContext() .doOnNext(sc -> { Authentication authentication = sc.getAuthentication(); assert authentication.getPrincipal().equals(username); assert authentication.getCredentials().equals(password); }) .then(Mono.empty()); });
然后简化StepVerifier的验证逻辑,只需确认流程完成即可:
StepVerifier.create(basicWebFilter.filter(exchange, chainMock)) .verifyComplete();
这种方式直接验证过滤器是否正确将上下文传递到了后续的过滤器链中,完全符合过滤器的职责,也不受下游操作符的影响。
2. 拆分职责进行测试
将测试拆分为两个独立单元:
- 单独测试
ServerAuthenticationConverter能否正确从请求中提取认证信息 - 测试过滤器是否正确将转换器的结果放入SecurityContext并传递给后续链
拆分后每个测试只验证单一职责,避免上下文传递的耦合问题,也让测试更稳定。
3. 调整过滤器实现(可选)
如果希望过滤器的错误处理不丢失上下文,可以在onErrorResume中使用Mono.defer来保留上下文,但这属于实现层面的调整,更推荐从测试策略层面解决问题。
内容的提问来源于stack exchange,提问作者Sergey Zolotarev

