1. 概述
跨域資源共享 (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 端點,以便它可以無需身份驗證地訪問。我們將使用此 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 生成一個隨機笑話和一個剩餘速率限制值。然後,我們返回笑話到響應主體中,幷包含 X-Rate-Limit-Remaining 標頭,其中包含生成的價值。
3. 測試 CORS 使用 MockMvc
現在我們已經配置了應用程序中的 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 錯誤。