跳到主要内容

16、测试

Testing Method Security

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

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

getMessage 的结果是一个对当前 Spring Security Authentication 说“Hello”的字符串。下面显示了一个输出示例。

你好 org.springframework.security.authentication.UsernamePasswordAuthenticationToken @ca25360:主体:org.springframework.security.core.userdetails.User @36ebcb:用户名:用户;密码保护]; 启用:真;AccountNonExpired:真;凭据非过期:真;AccountNonLocked:真;授予权限:ROLE_USER;凭证:[受保护];已认证:真实;详细信息:空;授予权限:ROLE_USER

9.1SecurityTestSetup; 在我们可以使用Spring Security Test支持之前,我们必须进行一些设置。一个例子可以在下面看到:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration 
public class WithMockUserTests {
This is a basic example of how to setup Spring Security Test. The highlights are:

@RunWith指示 spring-test 模块它应该创建一个 ApplicationContext 这与使用现有的 Spring Test 支持没有什么不同。有关更多信息,请参阅 Spring 参考

2 @ContextConfiguration指示 spring-test 用于创建 ApplicationContext 的配置。由于未指定配置,因此将尝试默认配置位置。这与使用现有的 Spring Test 支持没有什么不同。有关更多信息,请参阅 Spring 参考 [Note]

Spring Security hooks into Spring Test support using the WithSecurityContextTestExecutionListener 这将确保我们的测试由正确的用户运行。它通过在运行我们的测试之前填充 SecurityContextHolder 来做到这一点。测试完成后,会清空SecurityContextHolder。如果只需要 Spring Security 相关支持,可以替换@ContextConfiguration与@SecurityExecutionListeners. 记住我们添加了@PreAuthorize对我们的 HelloMessageService 进行注释,因此它需要经过身份验证的用户才能调用它。如果我们运行以下测试,我们预计以下测试将通过:

@测试(预期 = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
messageService.getMessage(); 

9.2 @WithMockUser;

问题是“我们怎样才能最容易地以特定用户的身份运行测试?” 答案是使用@WithMockUser. 以下测试将以用户名“user”、密码“password”和角色“ROLE_USER”的用户身份运行。

@测试
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
... } 具体来说,以下是正确的:

用户名“user”的用户不必存在,因为我们正在模拟用户 在 SecurityContext 中填充的身份验证的类型为 UsernamePasswordAuthenticationToken 身份验证的主体是 Spring Security 的用户对象 TheUser 将具有用户名“user”,密码“password”,并使用一个名为“ROLE_USER”的 GrantedAuthority。 我们的示例很好,因为我们能够利用很多默认值。如果我们想用不同的用户名运行测试怎么办?以下测试将使用用户名“customUser”运行。同样,用户不需要实际存在。

@测试
@WithMockUser(“customUsername”)
public void getMessageWithMockUserCustomUsername() {
String message = messageService.getMessage();
... } 我们还可以轻松自定义角色。例如,将使用用户名“admin”以及角色“ROLE_USER”和“ROLE_ADMIN”调用此测试。
@测试
@WithMockUser(用户名=”admin”,roles={“USER”,”ADMIN”})
public void getMessageWithMockUserCustomUser() {
String message = messageService.getMessage();
... } 如果我们不希望值自动以 ROLE_ 为前缀,我们可以利用 authority 属性。例如,将使用用户名“admin”和权限“USER”和“ADMIN”调用此测试。
@测试
@WithMockUser(用户名=“管理员”,权限={“管理员”,“用户”})
公共无效getMessageWithMockUserCustomAuthorities(){
字符串消息= messageService.getMessage();
... } 当然,将注释放在每个测试方法上可能有点乏味。相反,我们可以将注释放在类级别,每个测试都将使用指定的用户。例如,以下将使用用户名“admin”、密码“password”以及角色“ROLE_USER”和“ROLE_ADMIN”的用户运行每个测试。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username=”admin”,roles={“USER”,”ADMIN”})
public class WithMockUserTests {

9.3 @WithUserDetails;

而@WithMockUser是一种非常方便的入门方式,它可能不适用于所有情况。例如,应用程序通常期望 Authentication 主体是特定类型。这样做是为了让应用程序可以将主体引用为自定义类型并减少 Spring Security 的耦合。

自定义主体通常由自定义 UserDetailsS​​ervice 返回,该服务返回实现 UserDetails 和自定义类型的对象。对于这种情况,使用自定义 UserDetailsS​​ervice 创建测试用户很有用。这正是@WithUserDetails做。

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

@测试
@WithUserDetails
公共无效 getMessageWithUserDetails() {
字符串消息 = messageService.getMessage();
... } 我们还可以自定义用于从我们的 UserDetailsS​​ervice 中查找用户的用户名。例如,此测试将使用从 UserDetailsS​​ervice 返回的用户名“customUsername”的主体执行。
@测试
@WithUserDetails(“customUsername”)
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
… } 喜欢@WithMockUser我们还可以将注释放在类级别,以便每个测试都使用相同的用户。然而不像@WithMockUser, @WithUserDetails要求用户存在。

9.4 @WithSecurityContext;

我们看到@WithMockUser如果我们不使用自定义身份验证主体,这是一个很好的选择。接下来我们发现@WithUserDetails将允许我们使用自定义 UserDetailsS​​ervice 来创建我们的身份验证主体,但需要用户存在。我们现在将看到一个允许最大灵活性的选项。

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

@WithSecurityContext(工厂 = WithMockCustomUserSecurityContextFactory.class)
公共@interfaceWithMockCustomUser {

String username() default "rob";

String name() default "Rob Winch"; 你可以看到@WithMockCustomUser用@WithSecurityContext注释注解。这是向 Spring Security Test 支持发出的信号,表明我们打算为测试创建一个 SecurityContext。@WithSecurityContext _注释要求我们指定一个 SecurityContextFactory,它将根据我们的@WithMockCustomUser创建一个新的 SecurityContext注解。您可以在下面找到我们的 WithMockCustomUserSecurityContextFactory 实现:

公共类 WithMockCustomUserSecurityContextFactory
实现 WithSecurityContextFactory {
@Override
公共 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; 
}

我们现在可以用我们的新注解来注解一个测试类或一个测试方法,Spring Security 的 WithSecurityContextTestExcecutionListener 将确保我们的 SecurityContext 被适当地填充。

在创建自己的 WithSecurityContextFactory 实现时,很高兴知道它们可以使用标准 Spring 注释进行注释。例如,WithUserDetailsS​​ecurityContextFactory 使用@Autowired获取 UserDetailsS​​ervice 的

注释:

最终类 WithUserDetailsS​​ecurityContextFactory
实现 WithSecurityContextFactory {

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

1.SpringMVCTestIntegration;

Spring Security 提供与 Spring MVC Test 的全面集成

10.1SettingUpMockMvcandSpringSecurity;

为了在Spring MVC Test中使用Spring Security,需要添加Spring Security FilterChainProxy作为Filter。还需要添加 Spring Security 的 TestSecurityContextHolderPostProcessor 以支持在 Spring MVC Test with Annotations 中以用户身份运行。这可以使用 Spring Security 的 SecurityMockMvcConfigurers.springSecurity() 来完成。例如:

[注意] Spring Security 的测试支持需要 spring-test-4.1.3.RELEASE 或更高版本。 导入静态 org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
公共类 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 集成所需的所有初始设置

10.2SecurityMockMvcRequestPostProcessors;

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

导入静态 org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;

10.2.1TestingwithCSRFProtection;

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

mvc .perform(post(“/”).with(csrf()))

如果你愿意,你可以在 header 中包含 CSRF 令牌:

mvc .perform(post(“/”).with(csrf().asHeader()))

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

mvc .perform(post(“/”).with(csrf().useInvalidToken()))

10.2.2RunningaTestasaUserinSpringMVCTest;

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

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

10.2.3RunningasaUserinSpringMVCTestwithRequestPostProcessor;

有许多选项可用于填充测试用户。例如,以下将作为用户(不需要存在)运行,用户名为“user”,密码为“password”,角色为“ROLE_USER”:

mvc .perform(get(“/”).with(user(“user”)))

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

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

如果你有一个你想要的自定义 UserDetails使用,您也可以轻松地指定它。例如,以下将使用指定的 UserDetails(不需要存在)以使用具有指定 UserDetails 的主体的 UsernamePasswordAuthenticationToken 运行:

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

如果您想要自定义身份验证(不需要存在),您可以使用以下方法:

mvc .perform(get(“/”).with(authentication(authentication)))

您甚至可以使用以下命令自定义 SecurityContext:

mvc .perform(get(“/”).with(securityContext(securityContext)))

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

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

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

公共静态 RequestPostProcessor rob() { return user(“rob”).roles(“ADMIN”); 现在您可以在 SecurityMockMvcRequestPostProcessors 上执行静态导入并在您的测试中使用它:

导入静态样本.CustomSecurityMockMvcRequestPostProcessors.*;

mvc .perform(get(“/”).with(rob()))

在带有注释的 Spring MVC 测试中以用户身份运行

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

@测试
@WithMockUser
public void requestProtectedUrlWithUser() throws Exception {
mvc .perform(get(“/“))
... } 或者,以下将使用用户名“user”、密码“password”和角色“ROLE_ADMIN”的用户运行测试:
@测试
@WithMockUser(roles=”ADMIN”)
public void requestProtectedUrlWithUser() throws Exception {
mvc .perform(get(“/“))
... } 

10.2.4TestingHTTPBasicAuthentication;

虽然始终可以使用 HTTP Basic 进行身份验证,但记住标头名称、格式和编码值有点乏味。现在这可以使用 Spring Security 的 httpBasic RequestPostProcessor 来完成。例如,下面的片段:

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

将尝试使用 HTTP Basic 来验证用户名“user”和密码“password”的用户,方法是确保在 HTTP 请求中填充了以下标头:

授权:Basic dXNlcjpwYXNzd29yZA==

10.3SecurityMockMvcRequestBuilders;

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

导入静态 org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;

10.3.1TestingFormBasedAuthentication;

您可以使用 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(“a”,”admin”).password(“p”,”pass”))

10.3.2TestingLogout;

虽然使用标准 Spring MVC 测试相当简单,但您可以使用 Spring Security 的测试支持使测试注销更容易。例如,以下将使用有效的 CSRF 令牌向“/logout”提交 POST:

mvc .perform(logout())

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

mvc .perform(logout(“/signout”))

10.4SecurityMockMvcResultMatchers;

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

导入静态 org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;

10.4.1UnauthenticatedAssertion;

有时,断言没有与 MockMvc 调用的结果相关联的经过身份验证的用户可能是有价值的。例如,您可能想要测试提交无效的用户名和密码并验证没有用户通过身份验证。您可以使用 Spring Security 的测试支持轻松地做到这一点,使用如下所示:

mvc .perform(formLogin().password(“无效”))
.andExpect(unauthenticated());

10.4.2AuthenticatedAssertion;

很多时候,我们必须断言经过身份验证的用户存在。例如,我们可能想要验证我们是否成功进行了身份验证。我们可以使用以下代码片段验证基于表单的登录是否成功:

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

如果我们想断言用户的角色,我们可以改进我们之前的代码,如下所示:

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

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

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

我们还可以组合断言:

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