Spring Boot 测试 CORS 跨域配置

1、简介

跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种安全机制,允许网页从一个源访问另一个源的资源。它由浏览器强制执行,以防止网站向不同域发出未经授权的请求。

在使用 Spring Boot 构建 Web 应用时,必须正确测试 CORS 配置,以确保应用能安全地与授权的源交互,同时阻止未经授权的源。

通常情况下,我们只有在应用部署后才会发现 CORS 问题。通过尽早测试 CORS 配置,可以在开发过程中发现并解决这些问题,从而节省时间和精力。

本文将带你了解讨如何使用 MockMvc 编写有效的测试来验证 CORS 配置。

关于 Spring Boot 中 CORS 跨域配置的详细内容,你可以参阅 “在 Spring 应用中处理 CORS 跨域” 和 “Spring 和 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 origin 的请求,使用 GET 方法、X-Baeldung-Key Header,并在响应中暴露 X-Rate-Limit-Remaining Header。

本例在配置代码中硬编码了这些值,实际使用中,推荐通过 @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 端点列入了白名单,无需认证即可访问。我们将以此 API 端点为基础测试 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 随机生成一个笑话和一个剩余速率限制(Rate Limit)值。然后,在响应体中返回笑话,并在 X-Rate-Limit-Remaining Header 中包含生成的速率限制值。

3、使用 MockMvc 测试 CORS

在应用中配置了 CORS 后,让我们编写一些测试来确保它按预期运行。

使用 MockMvc 向 API 端点发送请求并验证响应。

3.1、测试允许的 Origin

首先,测试从允许的 Origin(源)发出的请求是否成功:

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 Header,以确定请求来自允许的 Origin。

接着,验证来自非允许的 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 Header。

同样,也要确保不允许使用的方法会被拒绝:

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

3.3、测试允许的 Header

现在,通过发送带有 Access-Control-Request-Headers Header 信息的预检请求,并验证响应中的 Access-Control-Allow-Headers 来测试允许的 Header 信息:

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

验证应用是否会拒绝不允许的 Header:

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、测试暴露的 Header

最后,测试暴露的 Header 是否正确地包含在允许的 Origin 的响应中:

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 Header 是否存在于响应中,并包含暴露的 X-Rate-Limit-Remaining Header。还要检查实际的 X-Rate-Limit-Remaining Header 是否存在。

同样,也需要确保暴露的 Header 不包含在非允许的 Origin 的响应中:

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 配置是否生效,包括测试允许的 Origin、允许的请求 Header、允许的请求方法以及暴露的响应 Header,同时阻止未经授权的请求。

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


Ref:https://www.baeldung.com/spring-boot-test-cross-origin-resource-sharing