【问题标题】:What pieces are needed to test a Spring Security Filter in Spring Boot?在 Spring Boot 中测试 Spring Security Filter 需要哪些部分?
【发布时间】:2018-02-06 06:21:16
【问题描述】:

所以我正在尝试测试我的安全过滤器,但我觉得我遗漏了一些明显的东西。这段代码的某些部分在过去有效,我将我的项目分成几个库(并从 maven 迁移到 gradle),所以很可能我已经离开了类路径中的依赖项或某些库。让我们从测试开始

@SpringBootTest
@AutoConfigureMockMvc
@RunWith( SpringRunner.class )
public class AuthenticationTest extends AbstractSecurityTest {
    @Autowired
    private MockMvc mvc;
    @Autowired
    private Gson gson;
    @SpyBean
    private JsonAuthenticationFilter filter;

    @Test
    public void validLogin() throws Exception {
        log.debug( "posting password" );
        String json = gson.toJson( new PasswordCredential( name, pass ) );
        this.mvc.perform(
            post( JsonAuthenticationFilter.AUTH_PASS )
                .contentType( MediaType.APPLICATION_JSON )
                .content( json ) )
            .andExpect( status().is2xxSuccessful() )
            .andExpect( cookie().exists( "SESSION" ) );

        verify( filter, times( 1 ) )
            .doFilter( any( ServletRequest.class ), any( ServletResponse.class ), any( FilterChain.class ) );
    }

    @Test
    public void invalidLogin() throws Exception {
        String json = gson.toJson( new PasswordCredential( "xenoterracide@gmail.com", "password" ) );
        this.mvc.perform(
            post( JsonAuthenticationFilter.AUTH_PASS )
                .contentType( MediaType.APPLICATION_JSON )
                .accept( MediaType.APPLICATION_JSON )
                .content( json ) )
            .andExpect( status().is4xxClientError() )
            .andExpect( status().isUnauthorized() );

        verify( filter, times( 1 ) )
            .doFilter( any( ServletRequest.class ), any( ServletResponse.class ), any( FilterChain.class ) );
    }

    @Test
    public void unsupportedMediaType() throws Exception {
        this.mvc.perform(
            post( JsonAuthenticationFilter.AUTH_PASS )
                .param( "pass", "password" )
                .param( "user", "xenoterracide@gmail.com" ) )
            .andExpect( status().is4xxClientError() );

        verify( filter, times( 1 ) )
            .doFilter( any( ServletRequest.class ), any( ServletResponse.class ), any( FilterChain.class ) );
    }
}

我的假设是 @AutoConfigureMockMvc 默认情况下应该包含过滤器。

这是我的身份验证过滤器

@Component
class JsonAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    static final String AUTH_PASS = Routes.PUBLIC + "/authentication/password";

    private static final RequestMatcher MATCHER = new AndRequestMatcher( Arrays.asList(
        new AntPathRequestMatcher( AUTH_PASS, HttpMethod.POST.name() ),
        new MediaTypeRequestMatcher( new ContentTypeContentNegotiationStrategy(),
            MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8 )
    ) );

    private final ObjectMapper objectMapper;
    private final Validator validator;

    protected JsonAuthenticationFilter( ObjectMapper objectMapper, Validator validator ) {
        super( MATCHER );
        this.objectMapper = objectMapper;
        this.validator = validator;
    }

    @Override
    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response )
        throws AuthenticationException, IOException {

        PasswordCredential credentials = objectMapper.readValue( request.getReader(), PasswordCredential.class );

        try {
            DataBinder dataBinder = new DataBinder( credentials );
            dataBinder.setBindingErrorProcessor( new DefaultBindingErrorProcessor() );
            dataBinder.setValidator( validator );
            dataBinder.validate();
            dataBinder.close();
        }
        catch ( BindException e ) {
            throw new BadRequestException( "field errors", e );
        }

        AbstractAuthenticationToken authRequest = credentials.toAuthenticationToken();

        setDetails( request, authRequest );

        return this.getAuthenticationManager().authenticate( authRequest );
    }

    private void setDetails( HttpServletRequest request, AbstractAuthenticationToken authRequest ) {
        authRequest.setDetails( authenticationDetailsSource.buildDetails( request ) );
    }

    @Override
    @Autowired( required = false )
    public void setRememberMeServices( RememberMeServices rememberMeServices ) {
        super.setRememberMeServices( rememberMeServices );
    }

    @Override
    @Autowired
    public void setAuthenticationManager( AuthenticationManager authenticationManager ) {
        super.setAuthenticationManager( authenticationManager );
    }
}

我知道它是@Autowired,并设置了AuthenticationManager,但没有调用继承的doFilter

plugins {
    `java-library`
    `maven-publish`
    `checkstyle`
    `idea`
    id("com.github.spotbugs").version("1.6.0")
    id("net.ltgt.errorprone").version("0.0.13")
    id("io.spring.dependency-management").version("1.0.4.RELEASE")
}

repositories {
    maven(System.getenv("JAR_REPOSITORY_URI"))
    jcenter()
}

group = "com.xenoterracide.rpf"
version = "0.1.0-SNAPSHOT"
description = "Security DTOs"

configurations.all({
    resolutionStrategy({
        cacheChangingModulesFor(1, TimeUnit.MINUTES)
    })
})
dependencyManagement {
    resolutionStrategy({
        cacheChangingModulesFor(1, TimeUnit.MINUTES)
    })
    imports {
        mavenBom("com.xenoterracide:bom:0.1.0-SNAPSHOT")
    }
}

// In this section you declare the dependencies for your production and test code
dependencies {
    errorprone("com.google.guava:guava:latest.release")
    errorprone("com.google.errorprone:error_prone_core:latest.release")

    compileOnly("com.google.code.findbugs:jsr305")
    compileOnly("com.google.errorprone:error_prone_annotations:latest.release")

    implementation("javax.validation:validation-api")
    implementation("javax.servlet:javax.servlet-api")
    implementation("com.fasterxml.jackson.core:jackson-annotations")
    implementation("com.fasterxml.jackson.core:jackson-core")
    implementation("com.fasterxml.jackson.core:jackson-databind")
    implementation("org.hibernate:hibernate-validator")
    implementation("org.springframework:spring-core")
    implementation("org.springframework:spring-context")
    implementation("org.springframework:spring-tx")
    implementation("org.springframework.data:spring-data-commons")
    implementation("org.springframework.data:spring-data-jpa")
    implementation("org.springframework.security:spring-security-core")
    implementation("org.springframework.security:spring-security-web")


    implementation("com.xenoterracide.rpf:constants:0.1.0-SNAPSHOT")
    implementation("com.xenoterracide.rpf:sec-dtos:0.1.0-SNAPSHOT")
    implementation("com.xenoterracide.rpf:sec:0.1.0-SNAPSHOT")

    testRuntimeOnly("com.h2database:h2")
    testRuntimeOnly("org.springframework.boot:spring-boot-starter-security")
    testRuntimeOnly("org.springframework.boot:spring-boot-starter-data-jpa")

    testImplementation("junit:junit")
    testImplementation("org.mockito:mockito-core")
    testImplementation("org.apache.commons:commons-lang3")
    testImplementation("com.google.code.gson:gson")
    testImplementation("org.assertj:assertj-core")
    testImplementation("org.springframework:spring-test")
    testImplementation("org.springframework.boot:spring-boot-test")
    testImplementation("org.springframework.boot:spring-boot-test-autoconfigure")

}

更新添加请求的类

class ContentTypeContentNegotiationStrategy implements ContentNegotiationStrategy {

    /**
     * {@inheritDoc}
     *
     * @throws HttpMediaTypeNotAcceptableException if the 'Content-Type' header cannot be parsed
     */
    @Override
    public List<MediaType> resolveMediaTypes( NativeWebRequest request )
        throws HttpMediaTypeNotAcceptableException {

        String[] headerValueArray = request.getHeaderValues( HttpHeaders.CONTENT_TYPE );
        if ( headerValueArray == null ) {
            return Collections.emptyList();
        }

        List<String> headerValues = Arrays.asList( headerValueArray );
        try {
            List<MediaType> mediaTypes = MediaType.parseMediaTypes( headerValues );
            MediaType.sortBySpecificityAndQuality( mediaTypes );
            return mediaTypes;
        }
        catch ( InvalidMediaTypeException ex ) {
            throw new HttpMediaTypeNotAcceptableException(
                "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage() );
        }
    }
}

以及测试失败的输出

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /v0/public/authentication/password
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8]}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 401
    Error message = Full authentication is required to access this resource
          Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], Strict-Transport-Security=[max-age=31536000 ; includeSubDomains], WWW-Authenticate=[Basic realm="Spring"]}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /v0/public/authentication/password
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8], Accept=[application/json]}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 401
    Error message = Full authentication is required to access this resource
          Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], Strict-Transport-Security=[max-age=31536000 ; includeSubDomains], WWW-Authenticate=[Basic realm="Spring"]}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /v0/public/authentication/password
       Parameters = {pass=[password], user=[xenoterracide@gmail.com]}
          Headers = {}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 401
    Error message = Full authentication is required to access this resource
          Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], Strict-Transport-Security=[max-age=31536000 ; includeSubDomains], WWW-Authenticate=[Basic realm="Spring"]}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []


Wanted but not invoked:
jsonAuthenticationFilter.doFilter(
    <any>,
    <any>,
    <any>
);
-> at com.xenoterracide.rpf.sec.authn.AuthenticationTest.unsupportedMediaType(AuthenticationTest.java:76)
Actually, there were zero interactions with this mock.

Wanted but not invoked:
jsonAuthenticationFilter.doFilter(
    <any>,
    <any>,
    <any>
);
-> at com.xenoterracide.rpf.sec.authn.AuthenticationTest.unsupportedMediaType(AuthenticationTest.java:76)
Actually, there were zero interactions with this mock.

    at com.xenoterracide.rpf.sec.authn.AuthenticationTest.unsupportedMediaType(AuthenticationTest.java:76)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)

我需要做什么才能让我的测试上下文调用过滤器?

【问题讨论】:

  • 我会假设您的请求匹配器不匹配。我想你可以很容易地调试来检查。您并没有真正说出ContentTypeContentNegotiationStrategy 在做什么。也许看看那里?
  • @DaveSyer line 199 of AbstractAuthenticationProcessingFilter.doFilter 调用 if (!requiresAuthentication(request, response)) { 在我的代码中实际上调用了 MATCHER,但从未调用过 doFilter,因此它永远不会命中该代码。虽然添加了那个类,以防万一
  • 那看起来很犹太。您可能必须创建一个独立的示例才能深入了解这一点。基本上,它适用于我,到目前为止我们所知道的作品。

标签: java spring spring-boot spring-security servlet-filters


【解决方案1】:

我缺少适当的WebSecurityAdapter 配置

@TestConfiguration
static class Config extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure( HttpSecurity http ) throws Exception {
        http.authorizeRequests()
            .mvcMatchers( JsonAuthenticationFilter.AUTH_PASS ).permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable();
    }
}

【讨论】:

  • 或者您可以更改过滤器的顺序,使其位于 Spring Security 之前。
  • @DaveSyer 我希望@Order 和“authorizeRequests”都需要较少的集中管理。我不能只注释一个控制器/方法@Authorize( ALL ),它会反射性地找出 mvc 路由吗? Order 的问题是它需要你记住一个整数顺序,而不是像 init 系统那样的相对“序列”(@Order( BEFORE, SpringSecurityFilter.class)。但是是的,尽管在我的原始配置中我放置在与 UserNamePasswordFilter 相同的位置.
猜你喜欢
  • 1970-01-01
  • 2018-01-02
  • 2012-07-17
  • 2017-07-08
  • 1970-01-01
  • 2011-12-22
  • 2016-10-06
  • 2021-02-08
  • 2017-10-21
相关资源
最近更新 更多