领先一步
VMware 提供培训和认证,为您的进步加速。
了解更多[callout title=于 2015 年 3 月 31 日更新]本文博客已过时,不再维护。请参阅参考文档中的测试部分以获取更新的文档。[/callout]
在我之前的博客中,我们演示了新的 Spring Security 测试支持如何简化基于方法的安全测试。在本博客中,我们将探讨如何在 Spring MVC Test 中使用该测试支持。
为了在 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]
Spring MVC Test 提供了一个方便的接口,称为 RequestPostProcessor
,可用于修改请求。Spring Security 提供了一些 RequestPostProcessor
实现,使测试更容易。为了使用 Spring Security 的 RequestPostProcessor
实现,请确保使用以下静态导入:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
测试任何非安全 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]
有多种选项可用于填充测试用户。例如,以下代码将以用户身份运行(该用户无需实际存在),其用户名为 "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 进行认证,但记忆请求头名称、格式和编码值有些繁琐。现在可以使用 Spring Security 的 httpBasic
RequestPostProcessor
来完成这项工作。例如,以下代码片段:
mvc
.perform(get("/").with(httpBasic("user","password")))
将尝试使用 HTTP Basic 认证一个用户,用户名为 "user",密码为 "password",通过确保在 HTTP 请求中填充以下请求头来实现:
Authorization: Basic dXNlcjpwYXNzd29yZA==
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"))
有时,需要对请求进行各种与安全相关的断言。为了满足这一需求,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"));
我们现在已经了解了 Spring Security 测试支持如何使 Spring MVC Test 更容易。在我们的下一篇博客中,我们将探讨如何结合 Spring Test MVC HtmlUnit 使用 Spring Security 测试支持。
[callout title=请提供反馈!] 如果您对本博客系列或 Spring Security 测试支持有任何反馈,我鼓励您通过 JIRA、评论区联系,或在 twitter 上通过 @rob_winch 私信我。当然,最好的反馈是以贡献的形式出现。[/callout]