1. 概述

跨域资源共享(CORS)是一种安全机制,允许网页从一个源访问另一个源的资源。浏览器通过强制执行CORS策略来防止网站向不同域发起未经授权的请求。

在Spring Boot应用开发中,正确测试CORS配置至关重要,这能确保我们的应用既能安全地与授权源交互,又能阻止未授权源的访问

很多时候,我们只有在应用部署后才发现CORS问题。通过在开发阶段早期测试CORS配置,我们可以在开发过程中就发现并解决这些问题,节省时间和精力

本教程将探讨如何使用MockMvc编写有效的测试,验证我们的CORS配置。

2. 在Spring Boot中配置CORS

在Spring Boot应用中配置CORS有多种方式。本教程中,我们将使用Spring Security并定义一个CorsConfigurationSource

private CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowedOrigins(List.of("https://baeldung.com"));
    corsConfiguration.setAllowedMethods(List.of("GET"));
    corsConfiguration.setAllowedHeaders(List.of("X-Baeldung-Key"));
    corsConfiguration.setExposedHeaders(List.of("X-Rate-Limit-Remaining"));

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);
    return source;
}

在配置中,我们允许来自https://baeldung.com源的请求,仅限GET方法,允许X-Baeldung-Key请求头,并在响应中暴露X-Rate-Limit-Remaining

虽然这里我们硬编码了值,但实际项目中可以使用@ConfigurationProperties进行外部化配置。

接下来,配置SecurityFilterChain bean以应用我们的CORS配置:

private static final String[] WHITELISTED_API_ENDPOINTS = { "/api/v1/joke" };

@Bean
public SecurityFilterChain configure(HttpSecurity http) {
    http
      .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource()))
      .authorizeHttpRequests(authManager -> {
        authManager.requestMatchers(WHITELISTED_API_ENDPOINTS)
          .permitAll()
          .anyRequest()
          .authenticated();
      });
    return http.build();
}

这里我们使用之前定义的corsConfigurationSource()方法配置CORS。

我们还白名单化了/api/v1/joke接口,使其无需认证即可访问。我们将以此接口为基础测试CORS配置

private static final Faker FAKER = new Faker();

@GetMapping(value = "/api/v1/joke")
public ResponseEntity<JokeResponse> generate() {
    String joke = FAKER.joke().pun();
    String remainingLimit = FAKER.number().digit();

    return ResponseEntity.ok()
      .header("X-Rate-Limit-Remaining", remainingLimit)
      .body(new JokeResponse(joke));
}

record JokeResponse(String joke) {};

我们使用Datafaker生成随机笑话和剩余速率限制值,然后在响应体中返回笑话,并在响应头中包含X-Rate-Limit-Remaining

3. 使用MockMvc测试CORS

现在应用已配置好CORS,让我们编写测试验证其行为。我们将使用MockMvc向API接口发送请求并验证响应

3.1. 测试允许的源

首先测试来自允许源的请求是否成功

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://baeldung.com"))
  .andExpect(status().isOk())
  .andExpect(header().string("Access-Control-Allow-Origin", "https://baeldung.com"));

我们同时验证响应中包含Access-Control-Allow-Origin头。

接下来验证非允许源的请求被阻止:

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://non-baeldung.com"))
  .andExpect(status().isForbidden())
  .andExpect(header().doesNotExist("Access-Control-Allow-Origin"));

3.2. 测试允许的方法

测试允许的方法时,我们使用HTTP OPTIONS方法模拟预检请求

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://baeldung.com")
  .header("Access-Control-Request-Method", "GET"))
  .andExpect(status().isOk())
  .andExpect(header().string("Access-Control-Allow-Methods", "GET"));

我们验证请求成功且响应中包含Access-Control-Allow-Methods头。

类似地,确保非允许的方法被拒绝:

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://baeldung.com")
  .header("Access-Control-Request-Method", "POST"))
  .andExpect(status().isForbidden());

3.3. 测试允许的请求头

**现在通过发送包含Access-Control-Request-Headers的预检请求,验证响应中的Access-Control-Allow-Headers**:

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://baeldung.com")
  .header("Access-Control-Request-Method", "GET")
  .header("Access-Control-Request-Headers", "X-Baeldung-Key"))
  .andExpect(status().isOk())
  .andExpect(header().string("Access-Control-Allow-Headers", "X-Baeldung-Key"));

验证应用拒绝非允许的请求头:

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://baeldung.com")
  .header("Access-Control-Request-Method", "GET")
  .header("Access-Control-Request-Headers", "X-Non-Baeldung-Key"))
  .andExpect(status().isForbidden());

3.4. 测试暴露的响应头

最后测试暴露的响应头是否正确包含在允许源的响应中

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://baeldung.com"))
  .andExpect(status().isOk())
  .andExpect(header().string("Access-Control-Expose-Headers", "X-Rate-Limit-Remaining"))
  .andExpect(header().exists("X-Rate-Limit-Remaining"));

我们验证响应中包含Access-Control-Expose-Headers头,且值为X-Rate-Limit-Remaining,同时检查实际的X-Rate-Limit-Remaining头是否存在。

类似地,确保非允许源的响应中不包含暴露的头:

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://non-baeldung.com"))
  .andExpect(status().isForbidden())
  .andExpect(header().doesNotExist("Access-Control-Expose-Headers"))
  .andExpect(header().doesNotExist("X-Rate-Limit-Remaining"));

4. 总结

本文探讨了如何使用MockMvc编写有效的测试,验证CORS配置是否正确允许授权的源、方法和请求头,同时阻止未授权的访问

通过全面测试CORS配置,我们可以及早发现配置错误,避免生产环境中出现意外的CORS问题。

本文的所有代码示例可在GitHub上获取:示例代码

⚠️ 踩坑提醒:测试CORS时务必区分普通请求和预检请求(OPTIONS),这是很多新手容易混淆的地方。


原始标题:Testing CORS in Spring Boot | Baeldung