1. 概述
跨域資源共享 (CORS) 是一種安全機制,允許一個網頁從一個源(origin)訪問另一個源的資源。 瀏覽器強制執行此機制,以防止網站向不同域發起未經授權的請求。
在構建 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. 使用 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 錯誤。