如何断言 ReactiveSecurityContextHolder 的内容?

问题描述 投票:0回答:1

注意。这个问题与我之前的问题相关,但有所不同。这个问题是关于 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 上下文初始化)

java testing spring-security
1个回答
0
投票

经过几天无果的尝试和痛苦的调试后,我想出了这个可行的解决方案

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();
    }
}

如果您有任何意见,我仍然期待您的意见

© www.soinside.com 2019 - 2024. All rights reserved.