摘要
CORS(跨域资源共享)是Web开发中几乎每个Java后端开发者都会遇到的“拦路虎”。当你在本地运行前端项目调用接口时,突然看到浏览器控制台报出熟悉的红色错误——No 'Access-Control-Allow-Origin' header is present——这就是CORS问题在“抗议”了。本文将从跨域问题的本质出发,系统讲解CORS的工作原理,然后深入Spring Boot、Spring Security、JAX-RS等主流Java框架中的解决方案,最后讨论生产环境下的最佳实践和常见踩坑点。
1. 为什么要跨域?同源策略的本质
1.1 什么是同源策略?
在理解跨域之前,先要理解同源策略(Same-Origin Policy)——这是浏览器最重要的安全机制之一。
同源的定义:两个URL的协议、域名、端口三者完全一致,才算同源。
| URL A | URL B | 是否同源 | 原因 |
|---|---|---|---|
https://example.com:443/page | https://example.com:443/api | ✅ 是 | 协议、域名、端口一致 |
http://example.com/page | https://example.com/api | ❌ 否 | 协议不同(http vs https) |
https://example.com/page | https://api.example.com/api | ❌ 否 | 域名不同(子域名不同) |
https://example.com:443/page | https://example.com:8080/api | ❌ 否 | 端口不同 |
同源策略的限制:非同源的请求会被浏览器拦截,主要限制三类行为:
DOM访问限制:无法读取跨域页面的DOM
Cookie/Cache限制:无法共享跨域的Cookie、LocalStorage
网络请求限制:AJAX/Fetch请求无法获取跨域响应(这是本文重点关注的内容)
1.2 跨域不等于安全漏洞
一个常见的误解是:“浏览器为什么要阻止跨域?是不是跨域就是错误的?”
恰恰相反。如果没有同源策略,恶意网站evil.com就能轻松访问你在bank.com上的会话Cookie,从而伪造你发起转账请求。同源策略保护的是用户在不同网站之间的身份隔离。
但在前后端分离的架构下,前端(http://localhost:3000)需要调用后端API(http://localhost:8080),跨域请求是刚需。这就引出了CORS——一个受控的、安全的跨域共享机制。
1.3 错误信息长什么样?
当跨域请求被拦截时,浏览器控制台会输出类似信息:
text
Access to XMLHttpRequest at 'http://localhost:8080/api/user' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
注意:请求实际上已经到达后端服务器并且执行了(比如数据库已经写入),只是浏览器拦截了响应。这是一个常见误区——后端认为一切正常,前端却报错。
2. CORS工作原理:浏览器与服务器的握手协议
CORS(Cross-Origin Resource Sharing,跨域资源共享)通过HTTP头来协商跨域请求的合法性,整个过程由浏览器自动发起,不需要前端代码额外处理。
2.1 简单请求 vs 预检请求
CORS将请求分为两类:
简单请求(Simple Request)
同时满足以下条件:
方法为
GET、HEAD、POST之一仅包含CORS安全的头:
Accept、Accept-Language、Content-Language、Content-Type(仅限application/x-www-form-urlencoded、multipart/form-data、text/plain)
简单请求的流程:浏览器直接发送请求,响应中必须包含Access-Control-Allow-Origin,否则浏览器拦截。
text
浏览器 ── GET /api/data (Origin: http://frontend.com) ──→ 服务器 浏览器 ←─ 200 OK (Access-Control-Allow-Origin: http://frontend.com) ── 服务器
预检请求(Preflight Request)
不满足简单请求条件的,会先发送一次OPTIONS请求“探路”:
方法为
PUT、DELETE、PATCH等使用自定义头(如
Authorization、X-Requested-With)Content-Type为application/json
流程:
text
浏览器 ── OPTIONS /api/data (Origin, Access-Control-Request-Method) ──→ 服务器 浏览器 ←─ 204/200 (Access-Control-Allow-*) ── 服务器(预检通过) 浏览器 ── PUT /api/data (真实的请求) ──→ 服务器 浏览器 ←─ 响应 ── 服务器
2.2 核心响应头解析
| 响应头 | 作用 | 示例 |
|---|---|---|
Access-Control-Allow-Origin | 允许哪些源访问 | *或https://frontend.com |
Access-Control-Allow-Methods | 允许哪些HTTP方法 | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | 允许哪些自定义头 | Authorization, Content-Type |
Access-Control-Expose-Headers | 允许前端读取哪些响应头 | X-Total-Count |
Access-Control-Allow-Credentials | 是否允许携带Cookie/凭证 | true |
Access-Control-Max-Age | 预检结果缓存时间(秒) | 3600 |
2.3 带凭证的请求
如果前端需要发送Cookie或HTTP认证信息,需要:
前端设置:
fetch(url, { credentials: 'include' })或xhr.withCredentials = true后端响应头必须:
Access-Control-Allow-Origin不能为*,且必须明确设置Access-Control-Allow-Credentials: true
3. Java解决方案实战
3.1 Spring Boot:最优雅的方式
方式一:全局配置(推荐)
编写一个配置类,统一管理CORS策略:
java
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 所有路径 .allowedOriginPatterns( // 允许的源(支持通配符) "http://localhost:3000", "https://frontend.com" ) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") // 允许所有请求头 .allowCredentials(true) // 允许携带Cookie .maxAge(3600); // 预检缓存1小时 } }方式二:使用@CrossOrigin注解(局部配置)
在Controller或方法上直接添加注解:
java
@RestController @RequestMapping("/api") @CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true") public class UserController { @GetMapping("/user") @CrossOrigin(originPatterns = "https://*.example.com") // 方法级别覆盖 public User getUser() { return new User("张三"); } }方式三:使用CorsFilter(Filter级别)
精细控制,适合需要在Filter链条中提前处理的场景:
java
@Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.setAllowedOriginPatterns(Arrays.asList("http://localhost:3000")); config.addAllowedHeader("*"); config.addAllowedMethod("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); }3.2 Spring Security整合
如果项目中使用了Spring Security,CORS配置必须放在Security Filter之前:
java
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 启用CORS .csrf(csrf -> csrf.disable()) // 跨域请求通常需要关闭CSRF(或用Token代替) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(Arrays.asList("http://localhost:3000")); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }⚠️ 注意:启用
allowCredentials(true)后,Spring Security的CSRF保护可能会拦截跨域请求。通常的做法是:对无状态的REST API关闭CSRF,或使用JWT等Token机制进行认证。
3.3 JAX-RS / Jersey
使用Jersey框架时,通过过滤器实现:
java
@Provider public class CorsFilter implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { responseContext.getHeaders().add( "Access-Control-Allow-Origin", "http://localhost:3000"); responseContext.getHeaders().add( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); responseContext.getHeaders().add( "Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization"); responseContext.getHeaders().add( "Access-Control-Allow-Credentials", "true"); // 处理预检请求 if ("OPTIONS".equalsIgnoreCase(requestContext.getMethod())) { responseContext.setStatus(Status.OK.getStatusCode()); } } }3.4 Spring Cloud Gateway(网关层统一处理)
在微服务架构中,通常在网关层统一处理CORS:
yaml
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowed-origin-patterns: - "http://localhost:3000" allowed-methods: "*" allowed-headers: "*" allow-credentials: true max-age: 3600
4. 生产环境最佳实践
4.1 不要使用*(除非绝对必要)
java
// ❌ 不推荐:生产环境不要用* .allowedOrigins("*") // ✅ 推荐:明确指定允许的源 .allowedOrigins("https://frontend-prod.com", "https://admin.example.com") // ✅ 或者使用模式匹配 .allowedOriginPatterns("https://*.myapp.com")4.2 环境差异化配置
开发环境、测试环境、生产环境的CORS策略应该不同:
java
@Configuration public class CorsConfig { @Value("${cors.allowed.origins}") private String[] allowedOrigins; @Value("${cors.allow-credentials:false}") private boolean allowCredentials; @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins(allowedOrigins) .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(allowCredentials); } }; } }application-dev.yml:
yaml
cors: allowed-origins: "http://localhost:3000,http://localhost:8080" allow-credentials: true
application-prod.yml:
yaml
cors: allowed-origins: "https://app.example.com" allow-credentials: true
4.3 处理OPTIONS请求的性能优化
预检请求会增加一次网络往返。合理设置maxAge可以减少预检请求次数:
java
.maxAge(7200) // 缓存2小时,单位:秒
4.4 安全考量
不要暴露内部域名:CORS配置中不要出现
localhost或内部IP的生产配置谨防Credentials泄露:
allowCredentials(true)时,allowedOrigins不能为*最小权限原则:只开放必要的
AllowedMethods和AllowedHeaders
5. 常见问题排查指南
问题1:配置了CORS仍然报错
排查步骤:
打开浏览器开发者工具 → Network,查看OPTIONS预检请求的响应头
确认后端确实返回了正确的
Access-Control-Allow-Origin检查是否有其他过滤器/拦截器覆盖了CORS头
java
// 常见错误:项目中的自定义过滤器在CORS之前返回了响应 // 解决:确保CorsFilter是第一个执行的过滤器 @Order(Ordered.HIGHEST_PRECEDENCE) public class CorsFilter implements Filter { // ... }问题2:预检请求(OPTIONS)返回403
Spring Security会拦截OPTIONS请求,需要在Security配置中放行:
java
http.authorizeHttpRequests(auth -> auth .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // ... 其他规则 );
问题3:携带Cookie失败
前端和后端都需要配置:
前端:
credentials: 'include'或withCredentials: true后端:
allowCredentials(true)+allowedOrigins不能为*
问题4:Nginx反向代理后的CORS
如果后端前面有Nginx,可以配置Nginx直接返回CORS头,减轻后端压力:
nginx
location /api/ { add_header 'Access-Control-Allow-Origin' 'https://frontend.com' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always; if ($request_method = 'OPTIONS') { return 204; } proxy_pass http://backend:8080; }6. 总结:核心要点一览
| 场景 | 推荐方案 |
|---|---|
| Spring Boot单体应用 | WebMvcConfigurer全局配置 |
| 使用Spring Security | 配置http.cors()+ 放行OPTIONS |
| 微服务架构 | 网关层统一处理(Spring Cloud Gateway) |
| 开发环境 | 可使用*或宽泛模式,方便调试 |
| 生产环境 | 严格指定源,最小权限原则 |
| 需要携带Cookie | allowCredentials(true)+ 明确指定Origin |
| 性能敏感 | 设置合理的maxAge |
最终建议:
CORS不是Bug,也不是“跨域问题”的最终答案——它是浏览器与服务器之间的一道安全闸门。理解其原理后,你会发现绝大多数CORS问题只需要一个配置类就能解决。当遇到奇怪的问题时,永远记得先用浏览器开发者工具查看预检请求的请求/响应头,95%的问题在这一步就能找到答案。