1、简介 {#1简介}
跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种安全机制,允许网页从一个源访问另一个源的资源。它由浏览器强制执行,以防止网站向不同域发出未经授权的请求。
在使用 Spring Boot 构建 Web 应用时,必须正确测试 CORS 配置,以确保应用能安全地与授权的源交互,同时阻止未经授权的源。
通常情况下,我们只有在应用部署后才会发现 CORS 问题。通过尽早测试 CORS 配置,可以在开发过程中发现并解决这些问题,从而节省时间和精力。
本文将带你了解讨如何使用 MockMvc 编写有效的测试来验证 CORS 配置。
关于 Spring Boot 中 CORS 跨域配置的详细内容,你可以参阅 "在 Spring 应用中处理 CORS 跨域" 和 "Spring 和 CORS 跨域" 这两篇文章。
2、Spring Boot 配置 CORS {#2spring-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
origin 的请求,使用 GET 方法、X-Baeldung-Key
Header,并在响应中暴露 X-Rate-Limit-Remaining
Header。
本例在配置代码中硬编码了这些值,实际使用中,推荐通过 @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 随机生成一个笑话和一个剩余速率限制(Rate Limit)值。然后,在响应体中返回笑话,并在 X-Rate-Limit-Remaining
Header 中包含生成的速率限制值。
3、使用 MockMvc 测试 CORS {#3使用-mockmvc-测试-cors}
在应用中配置了 CORS 后,让我们编写一些测试来确保它按预期运行。
使用 MockMvc 向 API 端点发送请求并验证响应。
3.1、测试允许的 Origin {#31测试允许的-origin}
首先,测试从允许的 Origin(源)发出的请求是否成功:
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
Header,以确定请求来自允许的 Origin。
接着,验证来自非允许的 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、测试允许的方法 {#32测试允许的方法}
使用 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
Header。
同样,也要确保不允许使用的方法会被拒绝:
mockMvc.perform(options("/api/v1/joke")
.header("Origin", "https://baeldung.com")
.header("Access-Control-Request-Method", "POST"))
.andExpect(status().isForbidden());
3.3、测试允许的 Header {#33测试允许的-header}
现在,通过发送带有 Access-Control-Request-Headers
Header 信息的预检请求,并验证响应中的 Access-Control-Allow-Headers
来测试允许的 Header 信息:
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"));
验证应用是否会拒绝不允许的 Header:
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、测试暴露的 Header {#34测试暴露的-header}
最后,测试暴露的 Header 是否正确地包含在允许的 Origin 的响应中:
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
Header 是否存在于响应中,并包含暴露的 X-Rate-Limit-Remaining
Header。还要检查实际的 X-Rate-Limit-Remaining
Header 是否存在。
同样,也需要确保暴露的 Header 不包含在非允许的 Origin 的响应中:
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、总结 {#4总结}
本文介绍了如何使用 MockMvc 编写有效的测试,以验证 CORS 配置是否生效,包括测试允许的 Origin、允许的请求 Header、允许的请求方法以及暴露的响应 Header,同时阻止未经授权的请求。
通过全面测试 CORS 配置,可以及早发现配置错误,避免在生产中出现意外的 CORS 错误。
Ref:https://www.baeldung.com/spring-boot-test-cross-origin-resource-sharing