12. Spring MVC 测试集成

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

12.1 建立 MockMvc 和 Spring Security

为了在 Spring MVC 测试中使用 Spring Security,有必要添加 Spring Security FilterChainProxy 作为 Filter。还需要添加 Spring Security 的 TestSecurityContextHolderPostProcessor 以支持使用注解在 Spring MVC 测试中作为用户运行。这可以使用 Spring Security 的 SecurityMockMvcConfigurers.springSecurity() 来完成。例如:

[Note] 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 测试

12.2 SecurityMockMvcRequestPostProcessors

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

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

12.2.1 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()))

12.2.2 在 Spring MVC 测试中作为用户运行测试

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

  • 使用 RequestPostProcessor 在 Spring MVC 测试中作为用户运行
  • 使用注解在 Spring MVC 测试中作为用户运行

12.2.3 使用 RequestPostProcessor 在 Spring MVC 测试中作为用户运行

有多种选项可将用户关联到当前的 HttpServletRequest。例如,以下将以用户名 "user"、密码 "password" 和角色 "ROLE_USER" 作为用户(不需要存在)运行:

[Note] Note

该支持通过将用户关联到 HttpServletRequest 来工作。为了将请求关联到 SecurityContextHolder,需要确保 SecurityContextPersistenceFilter 与 MockMvc 实例关联。做这件事的几种方法是:

  • 调用 apply(springSecurity())
  • 添加 Spring Security 的 FilterChainProxy 到 MockMvc
  • 在使用 MockMvcBuilders.standaloneSetup 时,手动添加 SecurityContextPersistenceFilter 到 MockMvc 实例可能是有意义的。
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 一起运行,它具有指定 UserDetails 的主体:

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

可以使用以下方式作为匿名用户运行:

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

如果你与默认用户一起运行,并且希望作为匿名用户执行一些请求,则这一点尤其有用。

如果你希望使用自定义身份验证(不需要存在),你可以使用以下方法进行:

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 的类中指定以下:

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

现在,你可以在 SecurityMockMvcRequestPostProcessors 上执行静态导入,并在测试中使用该静态导入:

import static sample.CustomSecurityMockMvcRequestPostProcessors.*;

...

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

使用注解在 Spring MVC 测试中作为用户运行

作为使用 RequestPostProcessor 创建用户的替代方法,可以使用 小节 11, 测试方法安全 中描述的注解。例如,以下用户将使用用户名 "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("/"))
		...
}

12.2.4 测试 HTTP 基本身份验证

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

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

将尝试使用 HTTP 基本通过确保在 HTTP 请求中填充以下头部来使用用户名 "user" 和密码 "password" 对用户进行身份验证:

Authorization: Basic dXNlcjpwYXNzd29yZA==

12.3 SecurityMockMvcRequestBuilders

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

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

12.3.1 测试表单基本身份认证

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

mvc
	.perform(formLogin())

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

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

我们还可以自定义用户名和密码包含的参数名。例如,上面的请求被修改为在 HTTP 参数 "u" 中包括用户名,在 HTTP 参数 "p" 中包括密码。

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

12.3.2 测试注销

虽然使用标准 Spring MVC 测试相当简单,但是你可以使用 Spring Security 的测试支持来简化测试注销。例如,下面将提交一个带有有效 CSRF 令牌的 "/logout" POST:

mvc
	.perform(logout())

你还可以自定义要 post 到的 URL。例如,下面的代码段将提交一个带有有效 CSRF 令牌的 "/signout" 的 POST:

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

12.4 SecurityMockMvcResultMatchers

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

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

12.4.1 未身份认证的断言

有时,断言没有与 MockMvc 调用的结果相关联的身份验证用户可能是有价值的。例如,你可能想测试提交一个无效的用户名和密码,并验证没有验证用户。使用 Spring Security 的测试支持可以很容易地使用以下内容:

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

12.4.2 身份认证的断言

通常我们必须断言认证用户存在。例如,我们可能想验证我们已经成功认证了。我们可以通过以下代码片段验证基于表单的登录成功:

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

我们也可以对认证进行任意断言

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