9. Testing

本节描述了 Spring Security 提供的测试支持。

Tip

要使用 Spring Security 测试支持,您必须包括spring-security-test-5.1.2.RELEASE.jar作为项目的依赖项。

9.1 测试方法的安全性

本节演示了如何使用 Spring Security 的 Test 支持来测试基于方法的安全性。首先,我们引入一个MessageService,要求用户经过身份验证才能访问它。

public class HelloMessageService implements MessageService {

    @PreAuthorize("authenticated")
    public String getMessage() {
        Authentication authentication = SecurityContextHolder.getContext()
            .getAuthentication();
        return "Hello " + authentication;
    }
}

getMessage的结果是一个字符串,向当前的 Spring Security Authentication说“ Hello”。输出示例如下所示。

Hello org.springframew[emailprotected]ca25360: Principal: [emailprotected]: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

9.1.1 安全测试设置

在使用 Spring Security Test 支持之前,我们必须执行一些设置。下面是一个示例:

@RunWith(SpringJUnit4ClassRunner.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {

这是如何设置 Spring Security Test 的基本示例。重点是:

  • (1) @RunWith指示 Spring 测试模块应创建ApplicationContext。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅Spring Reference
  • (2) @ContextConfiguration指示对配置进行 Spring 测试以创建ApplicationContext。由于未指定任何配置,因此将尝试使用默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关其他信息,请参阅Spring Reference

Note

Spring Security 使用WithSecurityContextTestExecutionListener连接到 Spring Test 支持,这将确保我们的测试以正确的用户运行。它是通过在运行测试之前填充SecurityContextHolder来实现的。如果您使用反应式方法安全性,则还需要ReactorContextTestExecutionListener填充ReactiveSecurityContextHolder。测试完成后,它将清除SecurityContextHolder。如果只需要与 Spring Security 相关的支持,则可以将@ContextConfiguration替换为@SecurityTestExecutionListeners

请记住,我们在HelloMessageService上添加了@PreAuthorize注解,因此需要经过身份验证的用户才能调用它。如果我们运行以下测试,我们希望以下测试将通过:

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
    messageService.getMessage();
}

9.1.2 @WithMockUser

问题是“我们如何最轻松地以特定用户身份运行测试?”答案是使用@WithMockUser。以下测试将以用户名“ user”,密码“ password”和角色“ ROLE_USER”的用户身份运行。

@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}

具体来说,以下是正确的:

  • 用户名“ user”的用户不必存在,因为我们在模拟用户

  • SecurityContext中填充的Authentication的类型为UsernamePasswordAuthenticationToken

  • Authentication上的主体是 Spring Security 的User对象

  • User的用户名为“ user”,密码为“ password”,并且使用一个单独的GrantedAuthority,名称为“ ROLE_USER”。

我们的示例很好,因为我们能够利用很多默认值。如果我们想使用其他用户名运行测试该怎么办?以下测试将使用用户名“ customUser”运行。同样,用户不需要实际存在。

@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
    String message = messageService.getMessage();
...
}

我们还可以轻松自定义角色。例如,将使用用户名“ admin”以及角色“ ROLE_USER”和“ ROLE_ADMIN”调用此测试。

@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
    String message = messageService.getMessage();
    ...
}

如果我们不希望该值自动以 ROLE 作为前缀,则可以利用 Authority 属性。例如,将使用用户名“ admin”以及权限“ USER”和“ ADMIN”来调用此测试。

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

当然,将 Comments 放在每种测试方法上可能会有些乏味。相反,我们可以将 Comments 放置在类级别,并且每个测试都将使用指定的用户。例如,下面的代码将使用用户名“ admin”,密码“ password”以及角色“ ROLE_USER”和“ ROLE_ADMIN”的用户运行每个测试。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {

默认情况下,在TestExecutionListener.beforeTestMethod事件期间设置SecurityContext。这等效于在 JUnit 的@Before之前发生的情况。您可以将其更改为在TestExecutionListener.beforeTestExecution事件期间发生,该事件在 JUnit 的@Before之后但在调用测试方法之前。

@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

9.1.3 @WithAnonymousUser

使用@WithAnonymousUser允许以匿名用户身份运行。当您希望与特定用户一起运行大多数测试,但又希望以匿名用户身份运行一些测试时,这特别方便。例如,以下将使用@WithMockUser并以匿名用户身份匿名运行 withMockUser1 和 withMockUser2.

@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {

    @Test
    public void withMockUser1() {
    }

    @Test
    public void withMockUser2() {
    }

    @Test
    @WithAnonymousUser
    public void anonymous() throws Exception {
        // override default to run as anonymous user
    }
}

默认情况下,在TestExecutionListener.beforeTestMethod事件期间设置SecurityContext。这等效于在 JUnit 的@Before之前发生的情况。您可以将其更改为在TestExecutionListener.beforeTestExecution事件期间发生,该事件在 JUnit 的@Before之后但在调用测试方法之前。

@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

9.1.4 @WithUserDetails

@WithMockUser是一种非常方便的入门方法,但不一定在所有情况下都起作用。例如,应用程序通常期望Authentication主体是特定类型。这样做是为了使应用程序可以将委托人称为自定义类型,并减少 Spring Security 上的耦合。

自定义主体通常由自定义UserDetailsService返回,自定义UserDetailsService返回实现UserDetails和自定义类型的对象。对于这种情况,使用自定义UserDetailsService创建测试用户非常有用。正是@WithUserDetails所做的。

假设我们有一个UserDetailsService作为 bean 公开,下面的测试将使用类型UsernamePasswordAuthenticationTokenAuthentication和从UserDetailsService返回的用户名“ user”的主体来调用。

@Test
@WithUserDetails
public void getMessageWithUserDetails() {
    String message = messageService.getMessage();
    ...
}

我们还可以自定义用于从UserDetailsService查找用户的用户名。例如,将使用从用户名为“ customUsername”的UserDetailsService返回的委托人来执行此测试。

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

我们还可以提供一个明确的 bean 名称来查找UserDetailsService。例如,此测试将使用 Bean 名称为“ myUserDetailsService”的UserDetailsService查找“ customUsername”的用户名。

@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
    String message = messageService.getMessage();
    ...
}

@WithMockUser一样,我们也可以将 Comments 放在类级别,以便每个测试使用同一用户。但是,与@WithMockUser不同,@WithUserDetails要求用户存在。

默认情况下,在TestExecutionListener.beforeTestMethod事件期间设置SecurityContext。这等效于在 JUnit 的@Before之前发生的情况。您可以将其更改为在TestExecutionListener.beforeTestExecution事件期间发生,该事件在 JUnit 的@Before之后但在调用测试方法之前。

@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)

9.1.5 @WithSecurityContext

我们已经看到,如果不使用自定义Authentication主体,则@WithMockUser是一个很好的选择。接下来,我们发现@WithUserDetails允许我们使用自定义UserDetailsService创建我们的Authentication主体,但是要求用户存在。现在,我们将看到一个具有最大灵 Active 的选项。

我们可以创建自己的 Comments,该 Comments 使用@WithSecurityContext创建所需的任何SecurityContext。例如,我们可以创建一个名为@WithMockCustomUser的 Comments,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

    String username() default "rob";

    String name() default "Rob Winch";
}

您可以看到@WithMockCustomUser带有@WithSecurityContextComments。这就是向 Spring Security Test 支持发出 signal 的 signal,我们打算为该测试创建一个SecurityContext@WithSecurityContext注解要求我们指定SecurityContextFactory,从而根据@WithMockCustomUser注解创建一个新的SecurityContext。您可以在下面找到我们的WithMockCustomUserSecurityContextFactory实现:

public class WithMockCustomUserSecurityContextFactory
    implements WithSecurityContextFactory<WithMockCustomUser> {
    @Override
    public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        CustomUserDetails principal =
            new CustomUserDetails(customUser.name(), customUser.username());
        Authentication auth =
            new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities());
        context.setAuthentication(auth);
        return context;
    }
}

现在,我们可以使用新的 Comments 对测试类或测试方法进行 Comments,Spring Security 的WithSecurityContextTestExecutionListener将确保正确填充SecurityContext

创建自己的WithSecurityContextFactory实现时,很高兴知道可以使用标准的 SpringComments 对其进行 Comments。例如,WithUserDetailsSecurityContextFactory使用@AutowiredComments 获取UserDetailsService

final class WithUserDetailsSecurityContextFactory
    implements WithSecurityContextFactory<WithUserDetails> {

    private UserDetailsService userDetailsService;

    @Autowired
    public WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public SecurityContext createSecurityContext(WithUserDetails withUser) {
        String username = withUser.value();
        Assert.hasLength(username, "value() must be non-empty String");
        UserDetails principal = userDetailsService.loadUserByUsername(username);
        Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authentication);
        return context;
    }
}

默认情况下,在TestExecutionListener.beforeTestMethod事件期间设置SecurityContext。这等效于在 JUnit 的@Before之前发生的情况。您可以将其更改为在TestExecutionListener.beforeTestExecution事件期间发生,该事件在 JUnit 的@Before之后但在调用测试方法之前。

@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)

9.1.6 测试元 Comments

如果您经常在测试中重用同一用户,则不理想的是必须重复指定属性。例如,如果有许多与用户名为“ admin”且角色为ROLE_USERROLE_ADMIN的 Management 用户相关的测试,则您必须编写:

@WithMockUser(username="admin",roles={"USER","ADMIN"})

我们可以使用元 Comments,而不是在所有地方重复此操作。例如,我们可以创建一个名为WithMockAdmin的元 Comments:

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }

现在,我们可以像更详细的@WithMockUser一样使用@WithMockAdmin

元 Comments 可与上述任何测试 Comments 一起使用。例如,这意味着我们也可以为@WithUserDetails("admin")创建一个元 Comments。

9.2 Spring MVC 测试集成

Spring Security 提供与SpringMVC 测试的全面集成

9.2.1 设置 MockMvc 和 Spring Security

为了将 Spring Security 与 Spring MVC Test 一起使用,有必要将 Spring Security FilterChainProxy添加为Filter。还必须添加 Spring Security 的TestSecurityContextHolderPostProcessor以支持在带有 Comments 的 Spring MVC 测试中以用户身份运行。这可以使用 Spring Security 的SecurityMockMvcConfigurers.springSecurity()完成。例如:

Note

Spring Security 的测试支持需要 spring-test-4.1.3.RELEASE 或更高版本。

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class CsrfShowcaseTests {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @Before
    public void setup() {
        mvc = MockMvcBuilders
                .webAppContextSetup(context)
                .apply(springSecurity()) (1)
                .build();
    }

...
  • (1) SecurityMockMvcConfigurers.springSecurity()将执行将 Spring Security 与 Spring MVC Test 集成在一起所需的所有初始设置

9.2.2 SecurityMockMvcRequestPostProcessors

Spring MVC Test 提供了一个方便的接口,称为RequestPostProcessor,可用于修改请求。 Spring Security 提供了许多RequestPostProcessor实现,这些实现使测试更加容易。为了使用 Spring Security 的RequestPostProcessor实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;

使用 CSRF 保护进行测试

当测试任何非安全的 HTTP 方法并使用 Spring Security 的 CSRF 保护时,必须确保在请求中包括有效的 CSRF 令牌。使用以下命令将有效的 CSRF 令牌指定为请求参数:

mvc
    .perform(post("/").with(csrf()))

如果愿意,可以在标题中包含 CSRF 令牌:

mvc
    .perform(post("/").with(csrf().asHeader()))

您还可以使用以下方法测试提供的 CSRF 令牌无效:

mvc
    .perform(post("/").with(csrf().useInvalidToken()))

在 Spring MVC 测试中以用户身份运行测试

通常需要以特定用户身份运行测试。填充用户有两种简单的方法:

使用 RequestPostProcessor 在 Spring MVC 测试中以用户身份运行

有许多选项可用于将用户与当前HttpServletRequest关联。例如,以下将以用户名(用户),密码“ password”和角色“ ROLE_USER”的用户身份(不需要存在)运行:

Note

该支持通过将用户与HttpServletRequest关联而起作用。要将请求与SecurityContextHolder关联,您需要确保SecurityContextPersistenceFilterMockMvc实例关联。实现此目的的几种方法是:

  • Invoking apply(springSecurity())

  • 将 Spring Security 的FilterChainProxy添加到MockMvc

  • 使用MockMvcBuilders.standaloneSetup时,手动将SecurityContextPersistenceFilter添加到MockMvc实例可能很有意义

mvc
    .perform(get("/").with(user("user")))

您可以轻松进行自定义。例如,以下用户名(Management 员),用户名“ admin”,密码“ pass”以及角色“ ROLE_USER”和“ ROLE_ADMIN”将作为用户(不需要存在)运行。

mvc
    .perform(get("/admin").with(user("admin").password("pass").roles("USER","ADMIN")))

如果您有要使用的自定义UserDetails,也可以轻松指定。例如,以下代码将使用指定的UserDetails(不需要存在)与UsernamePasswordAuthenticationToken一起运行,该UsernamePasswordAuthenticationToken的主体为指定的UserDetails

mvc
    .perform(get("/").with(user(userDetails)))

您可以使用以下身份以匿名用户身份运行:

mvc
    .perform(get("/").with(anonymous()))

如果您使用默认用户运行,并希望以匿名用户身份执行一些请求,则此功能特别有用。

如果您想要一个自定义的Authentication(不需要存在),可以使用以下方法:

mvc
    .perform(get("/").with(authentication(authentication)))

您甚至可以使用以下方法自定义SecurityContext

mvc
    .perform(get("/").with(securityContext(securityContext)))

通过使用MockMvcBuilders的默认请求,我们还可以确保针对每个请求以特定用户身份运行。例如,以下用户名(Management 员),用户名“ admin”,密码“ password”和角色“ ROLE_ADMIN”将作为用户(不需要存在)运行:

mvc = MockMvcBuilders
        .webAppContextSetup(context)
        .defaultRequest(get("/").with(user("user").roles("ADMIN")))
        .apply(springSecurity())
        .build();

如果发现您在许多测试中使用的是同一用户,建议将用户移至某个方法。例如,您可以在自己的名为CustomSecurityMockMvcRequestPostProcessors的类中指定以下内容:

public static RequestPostProcessor rob() {
    return user("rob").roles("ADMIN");
}

现在,您可以对SecurityMockMvcRequestPostProcessors执行静态导入,并在测试中使用它:

import static sample.CustomSecurityMockMvcRequestPostProcessors.*;

...

mvc
    .perform(get("/").with(rob()))
在带有 Comments 的 Spring MVC 测试中以用户身份运行

作为使用RequestPostProcessor创建用户的替代方法,您可以使用第 9.1 节“测试方法安全性”中描述的 Comments。例如,以下将对具有用户名“ user”,密码“ password”和角色“ ROLE_USER”的用户运行测试:

@Test
@WithMockUser
public void requestProtectedUrlWithUser() throws Exception {
mvc
        .perform(get("/"))
        ...
}

或者,以下将使用用户名“ user”,密码“ password”和角色“ ROLE_ADMIN”的用户运行测试:

@Test
@WithMockUser(roles="ADMIN")
public void requestProtectedUrlWithUser() throws Exception {
mvc
        .perform(get("/"))
        ...
}

测试 HTTP 基本身份验证

虽然始终可以使用 HTTP Basic 进行身份验证,但是记住 Headers 名称,格式和对值进行编码有点繁琐。现在,可以使用 Spring Security 的httpBasic RequestPostProcessor完成此操作。例如,以下代码段:

mvc
    .perform(get("/").with(httpBasic("user","password")))

将通过确保在 HTTP 请求中填充以下 Headers,尝试使用 HTTP Basic 对用户名“ user”和密码“ password”进行身份验证:

Authorization: Basic dXNlcjpwYXNzd29yZA==

9.2.3 SecurityMockMvcRequestBuilders

Spring MVC Test 还提供了一个RequestBuilder接口,可用于创建测试中使用的MockHttpServletRequest。 Spring Security 提供了RequestBuilder的一些实现,可用来简化测试。为了使用 Spring Security 的RequestBuilder实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;

测试基于表单的身份验证

您可以使用 Spring Security 的测试支持轻松地创建一个请求,以测试基于表单的身份验证。例如,以下代码将使用用户名“ user”,密码“ password”和有效的 CSRF 令牌向“/login”提交 POST:

mvc
    .perform(formLogin())

定制请求很容易。例如,以下内容将使用用户名“ admin”,密码“ pass”和有效的 CSRF 令牌向“/auth”提交 POST:

mvc
    .perform(formLogin("/auth").user("admin").password("pass"))

我们还可以自定义包含用户名和密码的参数名称。例如,这是上述请求,已修改为包括 HTTP 参数“ u”上的用户名和 HTTP 参数“ p”上的密码。

mvc
    .perform(formLogin("/auth").user("u","admin").password("p","pass"))

Testing Logout

使用标准 Spring MVC Test 相当简单,但是您可以使用 Spring Security 的测试支持来简化测试注销。例如,以下代码将使用有效的 CSRF 令牌向“/logout”提交 POST:

mvc
    .perform(logout())

您还可以自定义要发布到的 URL。例如,下面的代码片段将使用有效的 CSRF 令牌向“/signout”提交 POST:

mvc
    .perform(logout("/signout"))

9.2.4 SecurityMockMvcResultMatchers

有时希望对请求做出各种与安全性有关的 assert。为了满足这一需求,Spring Security Test 支持实现了 Spring MVC Test 的ResultMatcher接口。为了使用 Spring Security 的ResultMatcher实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;

Unauthenticated Assertion

有时 assert 没有与MockMvc调用的结果相关联的经过身份验证的用户可能很有价值。例如,您可能要测试提交的用户名和密码无效,并验证没有用户通过身份验证。您可以使用 Spring Security 的测试支持轻松地执行以下操作:

mvc
    .perform(formLogin().password("invalid"))
    .andExpect(unauthenticated());

Authenticated Assertion

通常,我们必须 assert 已通过身份验证的用户存在。例如,我们可能要验证我们已成功验证。我们可以使用以下代码片段来验证基于表单的登录是否成功:

mvc
    .perform(formLogin())
    .andExpect(authenticated());

如果我们想 assert 用户的角色,我们可以优化我们以前的代码,如下所示:

mvc
    .perform(formLogin().user("admin"))
    .andExpect(authenticated().withRoles("USER","ADMIN"));

或者,我们可以验证用户名:

mvc
    .perform(formLogin().user("admin"))
    .andExpect(authenticated().withUsername("admin"));

我们还可以结合以下 assert:

mvc
    .perform(formLogin().user("admin").roles("USER","ADMIN"))
    .andExpect(authenticated().withUsername("admin"));

我们还可以对身份验证进行任意 assert

mvc
    .perform(formLogin())
    .andExpect(authenticated().withAuthentication(auth ->
        assertThat(auth).isInstanceOf(UsernamePasswordAuthenticationToken.class)));