Chris Oberle 2024-01-08
1. 简介
无需独立集成环境即可执行集成测试,是任何软件栈都应具备的重要能力。Spring Boot 与 Spring Security 的无缝集成,使得测试涉及安全层的组件变得非常简单。
在本篇快速教程中,我们将探讨如何使用 @WebMvcTest 和 @SpringBootTest 来执行启用了安全机制的集成测试。
2. 依赖项
首先,让我们引入示例所需的依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
其中:
spring-boot-starter-web、spring-boot-starter-security和spring-boot-starter-test分别提供了 Spring MVC、Spring Security 以及 Spring Boot 测试工具;spring-security-test则用于启用我们将在测试中使用的@WithMockUser注解。
3. Web 安全配置
我们的 Web 安全配置非常简单:
- 只有经过身份验证的用户才能访问路径匹配
/private/**的资源; - 路径匹配
/public/**的资源则对所有用户开放。
@Configuration
public class WebSecurityConfigurer {
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.withUsername("spring")
.password(passwordEncoder.encode("secret"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(request -> request.requestMatchers(new AntPathRequestMatcher("/private/**"))
.hasRole("USER"))
.authorizeHttpRequests(request -> request.requestMatchers(new AntPathRequestMatcher("/public/**"))
.permitAll())
.httpBasic(Customizer.withDefaults())
.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4. 方法级安全配置
除了上述基于 URL 路径的安全控制外,我们还可以通过额外的配置启用方法级安全:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfigurer
extends GlobalMethodSecurityConfiguration {
}
该配置启用了 Spring Security 的 @PreAuthorize / @PostAuthorize 等注解支持。如需了解更详细的方法安全机制,请参阅我们关于该主题的文章。
5. 使用 @WebMvcTest 测试控制器
在使用 @WebMvcTest 注解结合 Spring Security 时,MockMvc 会自动配置好必要的过滤器链,以便测试我们的安全配置。
由于 MockMvc 已经为我们配置妥当,我们可以直接在测试中使用 @WithMockUser 注解,而无需额外设置:
@RunWith(SpringRunner.class)
@WebMvcTest(SecuredController.class)
public class SecuredControllerWebMvcIntegrationTest {
@Autowired
private MockMvc mvc;
// ... 其他方法
@WithMockUser(value = "spring")
@Test
public void givenAuthRequestOnPrivateService_shouldSucceedWith200() throws Exception {
mvc.perform(get("/private/hello").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
注意:@WebMvcTest 会指示 Spring Boot 仅实例化 Web 层,而不是整个应用上下文。因此,使用 @WebMvcTest 的控制器测试运行速度通常比其他方式更快。
6. 使用 @SpringBootTest 测试控制器
若使用 @SpringBootTest 注解来测试受保护的控制器,则需要在设置 MockMvc 时显式地配置安全过滤器链。
推荐的方式是使用 SecurityMockMvcConfigurer 提供的静态方法 springSecurity():
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SecuredControllerSpringBootIntegrationTest {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
// ... 其他方法
@WithMockUser("spring")
@Test
public void givenAuthRequestOnPrivateService_shouldSucceedWith200() throws Exception {
mvc.perform(get("/private/hello").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
7. 使用 @SpringBootTest 测试受保护的方法
使用 @SpringBootTest 测试受方法级安全保护的业务方法时,无需额外配置。我们可以直接调用这些方法,并根据需要使用 @WithMockUser:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SecuredMethodSpringBootIntegrationTest {
@Autowired
private SecuredService service;
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void givenUnauthenticated_whenCallService_thenThrowsException() {
service.sayHelloSecured();
}
@WithMockUser(username="spring")
@Test
public void givenAuthenticated_whenCallServiceWithSecured_thenOk() {
assertThat(service.sayHelloSecured()).isNotBlank();
}
}
8. 使用 @SpringBootTest 与 TestRestTemplate 进行测试
TestRestTemplate 是编写受保护 REST 接口集成测试的便捷选择。
我们可以注入一个模板实例,并在请求受保护端点前设置认证凭据:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SecuredControllerRestTemplateIntegrationTest {
@Autowired
private TestRestTemplate template;
// ... 其他方法
@Test
public void givenAuthRequestOnPrivateService_shouldSucceedWith200() throws Exception {
ResponseEntity<String> result = template.withBasicAuth("spring", "secret")
.getForEntity("/private/hello", String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
}
}
TestRestTemplate 非常灵活,提供了多种与安全相关的实用选项。如需了解更多细节,请参阅我们关于 TestRestTemplate 的文章。
9. 结论
在本文中,我们探讨了多种执行启用了安全机制的集成测试的方法:
- 如何测试 MVC 控制器和 REST 端点;
- 如何测试受方法级安全保护的业务逻辑。
通过合理选择测试策略(如 @WebMvcTest、@SpringBootTest、TestRestTemplate 等),我们可以高效、准确地验证 Spring Security 在应用中的行为。