Spring Security 测试预览:Web 安全

工程 | Rob Winch | 2014 年 5 月 23 日 | ...

[callout title=于 2015 年 3 月 31 日更新]本文博客已过时,不再维护。请参阅参考文档中的测试部分以获取更新的文档。[/callout]

在我之前的博客中,我们演示了新的 Spring Security 测试支持如何简化基于方法的安全测试。在本博客中,我们将探讨如何在 Spring MVC Test 中使用该测试支持。

设置 MockMvc 和 Spring Security

为了在 Spring MVC Test 中使用 Spring Security,需要将 Spring Security 的 FilterChainProxy 添加为一个 Filter。例如:

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

  @Autowired
  private WebApplicationContext context;

  @Autowired
  private Filter springSecurityFilterChain;

  private MockMvc mvc;

  @Before
  public void setup() {
      mvc = MockMvcBuilders
              .webAppContextSetup(context)
              .addFilters(springSecurityFilterChain)
              .build();
  }

  ...

[callout title=源代码]您可以在 github 上找到本博客系列的完整源代码[/callout]

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 Token。在 Spring Security 测试支持之前,这相当具有挑战性。现在,您可以使用以下方法将有效的 CSRF token 指定为请求参数:

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

如果您愿意,可以改为将 CSRF token 包含在请求头中:

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

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

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

以用户身份运行测试

通常需要以特定用户身份运行测试。在 Spring Security 测试支持之前,这在基于 Web 的测试中非常具有挑战性,因为 SecurityContextHolder 会被 SecurityContextRepositoryFilter 修改。

现在有两种简单的方法来填充用户:

[callout title=注意] 当前以用户身份运行的测试支持在 Spring Security 的无状态模式下不起作用。此问题正在 SEC-2593 中跟踪。[/callout]

使用 RequestPostProcessor 填充测试用户

有多种选项可用于填充测试用户。例如,以下代码将以用户身份运行(该用户无需实际存在),其用户名为 "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(该用户无需实际存在)来运行一个 UsernamePasswordAuthenticationToken,该 Token 的主体就是指定的 UserDetails

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

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

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")))
        .addFilters(springSecurityFilterChain)
        .build();

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

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

现在,您可以在 SecurityMockMvcRequestPostProcessors 上进行静态导入,并在测试中使用它。

import static sample.CustomSecurityMockMvcRequestPostProcessors.*;

...

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

使用注解填充测试用户

作为使用 RequestPostProcessor 创建用户的替代方法,您可以使用我在之前博客文章中描述的注解。然而,为了使此方法生效,您必须指定一个 RequestPostProcessor,它会使用注解中的用户运行请求,如下所示:

mvc = MockMvcBuilders
        .webAppContextSetup(context)
        .addFilters(springSecurityFilterChain)
        .defaultRequest(get("/").with(testSecurityContext()))
        .build();

[callout title=注意] 不要忘记指定 WithSecurityContextTestExcecutionListener,如之前博客中所述。[/callout]

现在您可以使用基于方法的安全测试中描述的任何方法来运行测试。例如,以下代码将以用户身份运行测试,其用户名为 "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 Basic 认证

虽然一直都可以使用 HTTP Basic 进行认证,但记忆请求头名称、格式和编码值有些繁琐。现在可以使用 Spring Security 的 httpBasic RequestPostProcessor 来完成这项工作。例如,以下代码片段:

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

将尝试使用 HTTP Basic 认证一个用户,用户名为 "user",密码为 "password",通过确保在 HTTP 请求中填充以下请求头来实现:

Authorization: Basic dXNlcjpwYXNzd29yZA==

SecurityMockMvcRequestBuilders

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

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

测试基于表单的认证

您可以使用 Spring Security 的测试支持轻松创建一个请求来测试基于表单的认证。例如,以下代码将向 "/login" 发送一个 POST 请求,其中包含用户名为 "user",密码为 "password" 和一个有效的 CSRF token:

mvc
    .perform(formLogin())

自定义请求也很容易。例如,以下代码将向 "/auth" 发送一个 POST 请求,其中包含用户名为 "admin",密码为 "pass" 和一个有效的 CSRF token:

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

我们还可以自定义包含用户名和密码的参数名称。例如,这是上面请求的修改版本,将用户名包含在 HTTP 参数 "u" 中,密码包含在 HTTP 参数 "p" 中。

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

测试注销

虽然使用标准的 Spring MVC Test 进行测试相当简单,但您可以使用 Spring Security 的测试支持来简化注销测试。例如,以下代码将向 "/logout" 发送一个带有有效 CSRF token 的 POST 请求:

mvc
    .perform(logout())

您还可以自定义要 POST 的 URL。例如,以下代码片段将向 "/signout" 发送一个带有有效 CSRF token 的 POST 请求:

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

SecurityMockMvcResultMatchers

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

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

未认证断言

有时,断言 MockMvc 调用结果中没有关联已认证用户可能很有价值。例如,您可能想测试提交无效的用户名和密码,并验证没有用户被认证。使用 Spring Security 的测试支持,您可以轻松做到这一点,代码如下:

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

已认证断言

通常我们需要断言存在已认证的用户。例如,我们可能想验证是否成功进行了认证。我们可以使用以下代码片段来验证基于表单的登录是否成功:

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"));

使用 HtmlUnit

我们现在已经了解了 Spring Security 测试支持如何使 Spring MVC Test 更容易。在我们的下一篇博客中,我们将探讨如何结合 Spring Test MVC HtmlUnit 使用 Spring Security 测试支持。

[callout title=请提供反馈!] 如果您对本博客系列或 Spring Security 测试支持有任何反馈,我鼓励您通过 JIRA、评论区联系,或在 twitter 上通过 @rob_winch 私信我。当然,最好的反馈是以贡献的形式出现。[/callout]

订阅 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,为您的进步加速。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部