前后端分离架构普及之后,跨域问题几乎成了每个项目上线前必经的一道坎。单个应用时代在Controller上加一个@CrossOrigin注解就能解决,但到了微服务架构下,请求链路变成了浏览器、网关、下游服务三层,跨域配置放哪里、怎么放、放几份,都会直接影响最终响应头是否正确。配置不当的典型表现就是浏览器控制台报出Access-Control-Allow-Origin is missing或者重复出现的错误。这篇文章把跨域的原理讲透,再给出几种主流配置方案的完整代码和选型建议。

先弄懂CORS的工作机制,配置才不会瞎猜
CORS全称是Cross-Origin Resource Sharing,即跨域资源共享。它是浏览器的安全机制,注意是浏览器行为,不是服务器行为。服务器之间互相调用HTTP接口不存在跨域问题,只有浏览器发起的请求才会受到同源策略约束。所谓同源,指的是协议、域名、端口三者完全一致,任何一个不同就构成跨域。
CORS请求分为两类:简单请求和预检请求。满足以下条件的属于简单请求:请求方法是GET、POST或HEAD,并且请求头只包含Accept、Accept-Language、Content-Language、Content-Type等少数几个安全字段,其中Content-Type仅限text/plain、multipart/form-data、application/x-www-form-urlencoded三种。一旦前端通过axios设置了自定义请求头(比如常见的携带token的Authorization头),或者Content-Type是application/json的PUT、DELETE请求,浏览器就会先发一个OPTIONS方法的预检请求。
预检请求的流程是这样的:浏览器先向目标地址发OPTIONS请求,服务器返回Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers等响应头,浏览器校验通过后才发送真正的请求。很多人在这里踩第一个坑:服务器明明配置了允许跨域,浏览器还是报错,原因往往是服务端没处理OPTIONS请求,直接返回了404或405,预检阶段就失败了。所以任何CORS配置都必须保证OPTIONS请求能正常返回200并携带正确的响应头。
方案一:在网关层统一处理,微服务的推荐做法
在微服务架构中,最推荐的方式是把跨域配置收敛到网关层,由网关统一给所有下游服务的响应追加CORS头。这样做的好处显而易见:配置集中、一处修改全局生效、下游服务无需感知跨域概念。以Spring Cloud Gateway为例,通过YAML配置即可完成。
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "https://*.bbccb.com"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600这里有一个非常关键的细节:当allowCredentials设置为true,即允许携带Cookie时,allowedOrigins不能写成通配符星号,必须改用allowedOriginPatterns来配置域名模式。这是CORS规范层面的强制约束,浏览器会直接拒绝同时出现通配符源和凭证模式的响应。不少团队升级Spring Cloud版本后突然跨域失效,十有八九就是这个原因,新版本把不合法的配置直接抛异常,而不是静默忽略。
server {
listen 80;
server_name api.bbccb.com;
location / {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS';
add_header Access-Control-Allow-Headers 'Authorization,Content-Type';
add_header Access-Control-Max-Age 3600;
return 200;
}
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials true;
proxy_pass http://gateway_cluster;
}
}需要注意Nginx的add_header指令有一个隐晦的特性:当location内出现带if的分支时,外层的add_header可能不会继承到内层,所以要像上面那样在if块里重复声明。另外Access-Control-Allow-Origin写成固定的$http_origin回显方式虽然省事,但建议配合一个源白名单判断,避免任意来源都能携带Cookie访问。
方案二:服务层单独配置,全局Filter与局部注解的选择
如果架构比较简单,没有网关层,或者某些服务直接暴露给前端访问,就需要在服务内部处理跨域。Spring Boot提供了多种方式,最常见的是全局配置类。相比在每个Controller上加@CrossOrigin注解,全局Filter方式维护成本更低。
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(Collections.singletonList("https://*.bbccb.com"));
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}也可以实现WebMvcConfigurer接口来配置,代码如下。两种方式效果接近,但存在一个优先级差异:CorsFilter在Filter链中执行,而WebMvcConfigurer的配置在DispatcherServlet阶段生效。如果项目里还有其他Filter对请求做了包装或提前拦截,建议优先使用CorsFilter,它执行时机更靠前,能确保OPTIONS请求在业务拦截器之前就被正确响应。
@Configuration
public class WebCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("https://*.bbccb.com")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}最常见的坑:响应头重复导致浏览器报错
p>网关配了跨域,下游服务也配了跨域,这是微服务里出现频率最高的错误组合。请求经过两层后,响应里会同时出现两个Access-Control-Allow-Origin头,浏览器校验时发现该头不唯一,直接判定跨域失败,控制台提示The 'Access-Control-Allow-Origin' header contains multiple values。这种报错经常让人困惑,因为单独访问网关或单独访问服务都是正常的,只有链路串起来才出错。解决思路有两种。第一种是遵循职责单一原则,只在网关配置跨域,下游服务全部删掉CORS相关配置,包括容易被遗忘的@CrossOrigin注解和历史遗留的拦截器。第二种是利用网关的去重能力,Spring Cloud Gateway提供了DedupeResponseHeader过滤器,可以配置对特定响应头去重。
spring:
cloud:
gateway:
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST这个配置会让网关在转发响应时只保留第一个出现的指定头,避免重复。但从架构整洁的角度看,去重只是补救手段,长远还是应该明确跨域归属哪一层,避免多处配置互相污染。
另一个高频坑是自定义头未声明。前端在请求头里加了Authorization、X-Token之类的自定义字段,但服务端Access-Control-Allow-Headers没有包含它们,预检就会失败。排查时直接看浏览器Network面板里那个OPTIONS请求的响应头,比看任何日志都直观。此外还要留意响应状态码为401、500时CORS头是否还在,如果异常响应丢失了CORS头,浏览器报的错会掩盖真实错误码,建议把跨域处理放在过滤器链最前面,覆盖所有响应路径。
选型建议:根据架构分层做决策
综合来看,如果系统已经引入了网关组件,跨域治理应该完全收敛到网关层,下游服务保持零CORS配置,这是可维护性最好的方案。如果入口是Nginx,也可以直接在Nginx层解决,但要处理好OPTIONS请求和add_header的继承问题。只有那些绕过网关直接对外的独立服务,才需要在服务内部配置,此时统一采用CorsFilter全局配置,避免注解式配置散落各处。
最后强调一点,跨域是浏览器的安全策略,本质上不是后端的错误,而是前后端协作的契约问题。上线前建议用真实浏览器完整走一遍预检请求的流程,重点检查携带Cookie的请求、带自定义头的请求、以及异常状态码下的响应头,把这几类场景验证通过,跨域问题基本就一次性收敛了。