Spring Boot 集成测试中的 Spring Security

更新于 2025-12-30

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-webspring-boot-starter-securityspring-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. 使用 @SpringBootTestTestRestTemplate 进行测试

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@SpringBootTestTestRestTemplate 等),我们可以高效、准确地验证 Spring Security 在应用中的行为。