注意。这个问题与我之前的问题相关,但有所不同。这个问题是关于 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();
}
}
但是,测试有一个主要问题,它非常脆弱。以下是我如何修改过滤器的代码以使测试失败
.flatMap(authentication -> chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)))
.onErrorResume(Exception.class, t -> {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
});
测试失败,因为我断言最下游的运算符不再是
MonoFlatMap
(它以某种方式保留了原始的Context
)而是MonoOnErrorResume
(不)
有哪些更稳健的策略来测试
ReactiveSecurityContextHolder
的内容?如果可能的话,我宁愿不使用 Spring 并保持我的测试真正的单元(没有 Spring 上下文初始化)
经过几天无果的尝试和痛苦的调试后,我想出了这个可行的解决方案
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
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.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.handler.DefaultWebFilterChain;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
public class AuthenticationFilterTest {
@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();
WebHandler handler = mock(WebHandler.class);
given(handler.handle(exchange)).willReturn(Mono.empty());
WebFilter authenticationAssertingFilter = (e, c) -> ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(a -> a.getPrincipal().equals(username) && a.getCredentials().equals(password))
.switchIfEmpty(Mono.error(new AssertionError("No expected authentication")))
.flatMap(a -> c.filter(e));
WebFilterChain webFilterChain = new DefaultWebFilterChain(handler, List.of(
new BasicWebFilter(), authenticationAssertingFilter));
StepVerifier.create(webFilterChain.filter(exchange))
.verifyComplete();
}
}
如果您有任何意见,我仍然期待您的意见