跨域資源共享 (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"));
|