什么是跨域请求(CORS)
跨域的概念
跨域是指浏览器出于安全考虑,限制了从不同源(协议,域名,端口任一不同)的服务器请求资源,这是浏览器的同源策略(Same-Origin Policy)所导致的。
同源策略要求一下三个必须相同
- 协议相同(http/https)
- 域名相同
- 端口相同
为什么需要CORS
在实际开发中,前后端分离架构非常普遍,前端应用运行在一个域名下,后端 API 服务运行在另一个域名下,这时就会遇到跨域问题。
CORS(Cross-Origin Resource Sharing) 是一种 W3C 标准,允许服务器声明哪些源可以访问其资源,从而解决跨域问题。
CORS 的工作原理
简单请求与预检请求
浏览器将 CORS 请求分为两类:简单请求和预检请求。
简单请求条件:
- 方法为 GET、HEAD、POST 之一
- Content-Type 为 text/plain、multipart/form-data、application/x-www-form-urlencoded 之一
- 没有自定义头部
预检请求:
不满足简单请求条件的请求会先发送 OPTIONS 预检请求,获得服务器许可后再发送实际请求。
Spring Boot 中解决CORS的四种方式
方法一 使用@CrossOrigin 注解
在控制器方法上使用
@RestController
@RequestMapping("/api")
public class UserController {@GetMapping("/users")@CrossOrigin(origins = "http://localhost:3000")public List<User> getUsers() {return Arrays.asList(new User(1, "张三", "zhangsan@example.com"),new User(2, "李四", "lisi@example.com"));}@PostMapping("/users")@CrossOrigin(origins = "http://localhost:3000")public User createUser(@RequestBody User user) {// 创建用户逻辑return user;}
}
多路径模式配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {// API 路径配置registry.addMapping("/api/**").allowedOrigins("http://localhost:3000").allowedMethods("GET", "POST", "PUT", "DELETE").allowedHeaders("*").allowCredentials(true).maxAge(3600);// 管理后台路径配置registry.addMapping("/admin/**").allowedOrigins("https://admin.example.com").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").allowedHeaders("Authorization", "Content-Type", "X-Requested-With").allowCredentials(true).maxAge(1800);// 公开接口配置registry.addMapping("/public/**").allowedOrigins("*").allowedMethods("GET", "HEAD").allowedHeaders("Content-Type").maxAge(1800);}
}
环境特定的配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Value("${cors.allowed.origins:http://localhost:3000}")private String[] allowedOrigins;@Value("${cors.allowed.methods:GET,POST,PUT,DELETE,OPTIONS}")private String[] allowedMethods;@Value("${cors.max.age:3600}")private long maxAge;@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins(allowedOrigins).allowedMethods(allowedMethods).allowedHeaders("*").allowCredentials(true).maxAge(maxAge);}
}
对应的 application.yml
配置:
cors:allowed:origins: - "http://localhost:3000"- "https://staging.example.com"- "https://app.example.com"methods: "GET,POST,PUT,DELETE,OPTIONS"max:age: 3600
方法三 使用CorsFilter 过滤器
基础CorsFilter配置
@Configuration
public class GlobalCorsConfig {@Beanpublic CorsFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();// 允许的域名config.addAllowedOrigin("http://localhost:3000");config.addAllowedOrigin("https://example.com");// 允许的请求方法config.addAllowedMethod("GET");config.addAllowedMethod("POST");config.addAllowedMethod("PUT");config.addAllowedMethod("DELETE");config.addAllowedMethod("OPTIONS");// 允许的请求头config.addAllowedHeader("*");// 是否允许发送 Cookieconfig.setAllowCredentials(true);// 预检请求的有效期config.setMaxAge(3600L);UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", config);return new CorsFilter(source);}
}
高级CorsFilter配置
@Configuration
public class AdvancedCorsConfig {@Bean@Order(Ordered.HIGHEST_PRECEDENCE)public CorsFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();// 从配置文件中读取允许的源config.setAllowedOrigins(Arrays.asList("http://localhost:3000","https://admin.example.com","https://app.example.com"));// 设置允许的方法config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));// 设置允许的头部config.setAllowedHeaders(Arrays.asList("Authorization","Content-Type","X-Requested-With","Accept","Origin","Access-Control-Request-Method","Access-Control-Request-Headers","X-CSRF-TOKEN"));// 设置暴露的头部config.setExposedHeaders(Arrays.asList("Access-Control-Allow-Origin","Access-Control-Allow-Credentials","X-Custom-Header"));// 允许凭证config.setAllowCredentials(true);// 预检请求缓存时间config.setMaxAge(3600L);UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();// 为不同路径设置不同的 CORS 配置source.registerCorsConfiguration("/api/**", config);CorsConfiguration publicConfig = new CorsConfiguration();publicConfig.addAllowedOrigin("*");publicConfig.addAllowedMethod("GET");publicConfig.addAllowedHeader("Content-Type");source.registerCorsConfiguration("/public/**", publicConfig);return new CorsFilter(source);}
}
方式四 手动处理OPTIONS请求
在某些特殊情况下,你可能需要手动处理预检请求:
@Component
public class CustomCorsFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletResponse response = (HttpServletResponse) res;HttpServletRequest request = (HttpServletRequest) req;// 设置 CORS 头部response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With");response.setHeader("Access-Control-Allow-Credentials", "true");response.setHeader("Access-Control-Max-Age", "3600");// 如果是 OPTIONS 请求,直接返回 200if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {response.setStatus(HttpServletResponse.SC_OK);return;}chain.doFilter(req, res);}@Overridepublic void init(FilterConfig filterConfig) {// 初始化逻辑}@Overridepublic void destroy() {// 清理逻辑}
}
高级配置和最佳实践
环境特定的cors配置
@Configuration
@Profile("dev")
public class DevCorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("http://localhost:3000", "http://localhost:8080").allowedMethods("*").allowedHeaders("*").allowCredentials(true);}
}@Configuration
@Profile("prod")
public class ProdCorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/api/**").allowedOrigins("https://app.example.com", "https://admin.example.com").allowedMethods("GET", "POST", "PUT", "DELETE").allowedHeaders("Authorization", "Content-Type", "X-Requested-With").allowCredentials(true).maxAge(3600);}
}
动态CORS配置
@Component
public class DynamicCorsFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String origin = request.getHeader("Origin");if (isAllowedOrigin(origin)) {response.setHeader("Access-Control-Allow-Origin", origin);response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With");response.setHeader("Access-Control-Allow-Credentials", "true");response.setHeader("Access-Control-Max-Age", "3600");}if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {response.setStatus(HttpServletResponse.SC_OK);return;}filterChain.doFilter(request, response);}private boolean isAllowedOrigin(String origin) {// 从数据库或配置中心动态获取允许的源List<String> allowedOrigins = Arrays.asList("http://localhost:3000","https://app.example.com","https://admin.example.com");return allowedOrigins.contains(origin);}
}
安全最佳实践
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and() // 启用 CORS 支持.csrf().disable() // 根据需求决定是否禁用 CSRF.authorizeRequests().antMatchers("/api/public/**").permitAll().antMatchers("/api/admin/**").hasRole("ADMIN").anyRequest().authenticated().and().httpBasic();}@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration = new CorsConfiguration();// 生产环境中应该从配置文件中读取configuration.setAllowedOrigins(Arrays.asList("https://trusted-domain.com","https://app.trusted-domain.com"));configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));configuration.setExposedHeaders(Arrays.asList("X-Custom-Header"));configuration.setAllowCredentials(true);configuration.setMaxAge(3600L);UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/api/**", configuration);return source;}
}
测试CORS配置
测试控制器
@RestController
@RequestMapping("/api/test")
public class CorsTestController {@GetMapping("/cors-test")@CrossOrigin(origins = "http://localhost:3000")public ResponseEntity<Map<String, String>> corsTest() {Map<String, String> response = new HashMap<>();response.put("message", "CORS test successful");response.put("timestamp", Instant.now().toString());return ResponseEntity.ok(response);}@PostMapping("/cors-post")public ResponseEntity<Map<String, Object>> corsPostTest(@RequestBody TestRequest request) {Map<String, Object> response = new HashMap<>();response.put("received", request);response.put("processedAt", Instant.now().toString());return ResponseEntity.ok(response);}@PutMapping("/cors-put/{id}")public ResponseEntity<Map<String, Object>> corsPutTest(@PathVariable String id, @RequestBody TestRequest request) {Map<String, Object> response = new HashMap<>();response.put("id", id);response.put("data", request);response.put("updatedAt", Instant.now().toString());return ResponseEntity.ok(response);}
}class TestRequest {private String name;private String email;// getters and setterspublic String getName() { return name; }public void setName(String name) { this.name = name; }public String getEmail() { return email; }public void setEmail(String email) { this.email = email; }
}
前端测试代码
<!DOCTYPE html>
<html>
<head><title>CORS Test</title><script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body><h1>CORS Test Page</h1><button onclick="testGet()">Test GET</button><button onclick="testPost()">Test POST</button><button onclick="testPut()">Test PUT</button><div id="result"></div><script>const API_BASE = 'http://localhost:8080/api/test';async function testGet() {try {const response = await axios.get(`${API_BASE}/cors-test`, {withCredentials: true});document.getElementById('result').innerHTML = `<pre>${JSON.stringify(response.data, null, 2)}</pre>`;} catch (error) {document.getElementById('result').innerHTML = `<pre style="color: red">Error: ${error.message}</pre>`;}}async function testPost() {try {const response = await axios.post(`${API_BASE}/cors-post`, {name: 'Test User',email: 'test@example.com'}, {withCredentials: true,headers: {'Content-Type': 'application/json'}});document.getElementById('result').innerHTML = `<pre>${JSON.stringify(response.data, null, 2)}</pre>`;} catch (error) {document.getElementById('result').innerHTML = `<pre style="color: red">Error: ${error.message}</pre>`;}}async function testPut() {try {const response = await axios.put(`${API_BASE}/cors-put/123`, {name: 'Updated User',email: 'updated@example.com'}, {withCredentials: true,headers: {'Content-Type': 'application/json','X-Custom-Header': 'custom-value'}});document.getElementById('result').innerHTML = `<pre>${JSON.stringify(response.data, null, 2)}</pre>`;} catch (error) {document.getElementById('result').innerHTML = `<pre style="color: red">Error: ${error.message}</pre>`;}}</script>
</body>
</html>
常见问题与解决方案
CORS配置不生效
问题原因:
- 配置顺序问题
- 过滤器链顺序问题
- 安全配置冲突
解决方案:
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE) // 确保高优先级
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("http://localhost:3000").allowedMethods("*").allowedHeaders("*").allowCredentials(true);}
}
携带凭证(Credentials)问题
当使用 allowCredentials(true)
时,不能使用通配符 *
作为允许的源:
// 错误配置
.config.setAllowedOrigins(Arrays.asList("*")); // 与 allowCredentials(true) 冲突// 正确配置
config.setAllowedOrigins(Arrays.asList("http://localhost:3000", "https://example.com"));
总结
Spring Boot 提供了多种灵活的方式来处理 CORS 跨域请求:
-
@CrossOrigin 注解:适合细粒度的控制器或方法级别配置
-
WebMvcConfigurer:适合应用级别的全局配置
-
CorsFilter:提供最灵活的过滤器级别配置
-
手动处理:适合特殊需求的定制化处理
选择建议:
- 开发环境:使用宽松的全局配置
- 生产环境:使用严格的、基于配置的 CORS 策略
- 微服务架构:在 API 网关层统一处理 CORS
- 特殊需求:使用 CorsFilter 进行精细控制