Spring Boot 中測試 CORS

banq發表於2024-07-02

跨域資源共享 (CORS)是一種安全機制,允許來自一個來源的網頁訪問來自另一個來源的資源。瀏覽器強制執行該機制,以防止網站向不同的域發出未經授權的請求。

在使用 Spring Boot 構建 Web 應用程式時,正確測試我們的 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://jdon.com"));
    corsConfiguration.setAllowedMethods(List.of("GET"));
    corsConfiguration.setAllowedHeaders(List.of("X-jdon-Key"));
    corsConfiguration.setExposedHeaders(List.of("X-Rate-Limit-Remaining"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);
    return source;
}

在我們的配置中,我們允許來自https://jdon.com來源的請求,使用 GET 方法、X-jdon-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端點列入白名單,因此無需身份驗證即可訪問。我們將使用此 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生成一個隨機joke和一個剩餘速率限制值。然後我們在響應主體中返回笑話,並在X-Rate-Limit-Remaining標頭中包含生成的值。

使用 MockMvc 測試 CORS
現在我們已經在應用程式中配置了 CORS,讓我們編寫一些測試來確保它按預期工作。我們將使用MockMvc向我們的 API 端點傳送請求並驗證響應。

 測試允許的來源
首先,讓我們測試來自我們允許的來源的請求是否成功:

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

我們還驗證響應是否包含來自允許來源的請求的Access-Control-Allow-Origin標頭。

接下來,讓我們驗證來自非允許來源的請求是否被阻止:

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


測試允許的方法
為了測試允許的方法,我們將使用 HTTP OPTIONS 方法模擬預檢請求:

mockMvc.perform(options("/api/v1/joke")
  .header("Origin", "https://jdon.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://jdon.com")
  .header("Access-Control-Request-Method", "POST"))
  .andExpect(status().isForbidden());

測試允許的標頭
現在,我們將透過傳送帶有Access-Control-Request-Headers標頭的預檢請求並驗證響應中的Access-Control-Allow-Headers 來測試允許的標頭:

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

讓我們驗證一下我們的應用程式是否拒絕不允許的標頭:

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

測試暴露的標頭
最後,讓我們測試一下公開的標頭是否正確包含在允許來源的響應中:

mockMvc.perform(get("/api/v1/joke")
  .header("Origin", "https://jdon.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-jdon.com"))
  .andExpect(status().isForbidden())
  .andExpect(header().doesNotExist("Access-Control-Expose-Headers"))
  .andExpect(header().doesNotExist("X-Rate-Limit-Remaining"));

相關文章