Spring Security 测试预览:Web 安全

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

[侧边栏 title=2015年3月31日更新]此博客已过时,不再维护。请参阅参考文档的测试章节以获取更新的文档。[/侧边栏]

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

设置 MockMvc 和 Spring Security

为了将 Spring Security 与 Spring MVC Test 一起使用,必须添加 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();
  }

  ...

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

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

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

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

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

您还可以测试提供无效的 CSRF 令牌:

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

以用户身份运行测试

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

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

[侧边栏 title=注意] 使用 Spring Security 的无状态模式时,当前用户身份运行的测试支持当前不起作用。这在 SEC-2593 中进行跟踪。[/侧边栏]

使用 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`(无需存在)以具有指定 `UserDetails` 主体的 `UsernamePasswordAuthenticationToken` 运行:

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

如果您想要自定义的 `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();

[侧边栏 title=注意] 不要忘记按照之前的博客中所述指定 `WithSecurityContextTestExcecutionListener`。[/侧边栏]

现在,您可以使用基于方法的安全测试中描述的任何方法运行测试。例如,以下操作将使用用户名为“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 基本身份验证进行身份验证,但记住标头名称、格式和编码值有点繁琐。现在可以使用 Spring Security 的 `httpBasic` `RequestPostProcessor` 来完成此操作。例如,下面的代码片段:

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

将尝试使用 HTTP 基本身份验证来验证用户名为“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 的测试支持来测试基于表单的身份验证。例如,以下操作将使用用户名“user”、密码“password”和有效的 CSRF 令牌向“/login”提交 POST 请求。

mvc
    .perform(formLogin())

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

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

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

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

测试注销

虽然使用标准 Spring MVC Test 非常简单,但您可以使用 Spring Security 的测试支持来简化注销测试。例如,以下操作将使用有效的 CSRF 令牌向“/logout”提交 POST 请求。

mvc
    .perform(logout())

您还可以自定义要发布到的 URL。例如,下面的代码段将使用有效的 CSRF 令牌向“/signout”提交 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 Security 测试支持与Spring Test MVC HtmlUnit一起使用。

[边栏标题=请提供反馈!]如果您对本博客系列或 Spring Security 测试支持有任何反馈,我鼓励您通过JIRA、评论部分或在 Twitter 上联系我@rob_winch。当然,最好的反馈是贡献。[/边栏]

获取 Spring 电子报

与 Spring 电子报保持联系

订阅

领先一步

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

了解更多

获取支持

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

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部