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