
只要是前后端分离的项目尤其是Spring Boot充当前端接口服务时跨域报错基本是每个后端开发都绕不开的经历。浏览器控制台爆出一行No Access-Control-Allow-Origin header is present on the requested resource后台日志却干干净净接口也正常返回了数据。问题就出在浏览器这个“门卫”身上。跨域Cross-Origin字面意思就是“跨过源”这本身不是Spring Boot的bug而是浏览器基于同源策略做的安全限制。你后端把数据吐得再快浏览器也照样拦在门外给你看报错。这篇文章就从最常用的四种解决方式讲起CrossOrigin注解、全局配置WebMvcConfigurer、注册CorsFilter、以及通过Nginx反向代理在网关层处理。看完你能知道每种方案在什么场景下用还有那些网上很少讲明白的连带坑。1. 跨域报错的本质浏览器在拦你不是后端在拒绝你1.1 同源策略与“源”的组成先搞清楚一个概念什么叫“同源”。同源指的是协议、域名、端口三者完全一致。只要有一个不一样就是跨域。比如前端跑在http://localhost:5173后端跑在http://localhost:8080端口不同跨域前端是http://www.example.com后端是http://api.example.com域名不同跨域前端是https://www.example.com后端是http://www.example.com协议不同也算跨域。后端服务本身是没有跨域概念的。你拿Postman去请求接口随便怎么调都能通因为Postman不走浏览器。真正的拦截发生在浏览器这一层浏览器发出跨域请求后如果响应头里没有携带服务器允许的跨域声明浏览器就把响应扣下来在控制台抛给你看。很多初学者以为后端接口挂了其实数据早就回来了只是浏览器不让用。同源策略为什么存在本质上是为了保护用户的数据安全。想象一下如果任何站点都能随意读取其他域名的接口数据你登录了银行网站另一个恶意网站只要往银行接口发请求就能拿你的Cookie去操作账户。浏览器出于安全考虑默认禁止跨域读取响应内容。1.2 CORS请求分类简单请求与预检请求CORSCross-Origin Resource Sharing跨域资源共享机制是W3C的标准。要理解跨域解决方案必须先分清两种请求简单请求和预检请求。简单请求通常只满足两个条件请求方法是GET、HEAD、POST之一且请求头只能是Accept、Content-Type、Origin等几个标准字段Content-Type也仅限于application/x-www-form-urlencoded、multipart/form-data、text/plain。这种请求浏览器会直接发出。预检请求则复杂一些。比如你用了Content-Type: application/json或者带上了Authorization请求头浏览器就会先发一个OPTIONS请求去问后端“我接下来要跨域发一个带JSON的POST请求你允许吗”后端返回的响应头里有Access-Control-Allow-Origin等声明浏览器确认后才真正发出业务请求。所以你经常能在浏览器Network面板里看到同一个接口出现了两次请求一次是OPTIONS一次是真实的GET/POST这不是接口被调用了两次而是预检请求在探路。后端解决跨域的本质就是往响应头里塞Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers这些字段。Spring Boot的四种解决方案本质上都是做同一件事只是塞头的方式和位置不同。2. 方案一CrossOrigin注解最快捷但只适合小范围2.1 注解级配置的写法与参数含义CrossOrigin是Spring MVC从4.2版本开始提供的注解用起来非常直接。可以直接加在Controller类上对类里所有接口生效也可以加在具体方法上只对那一个接口生效。RestController RequestMapping(/api/user) CrossOrigin( origins http://localhost:5173, allowedMethods {GET, POST, PUT, DELETE, OPTIONS}, maxAge 3600 ) public class UserController { GetMapping(/info) public ResultString info() { return Result.success(ok); } }下面是几个主要参数的说明我用一张表列清楚参数作用默认值origins/value允许访问的源列表默认允许所有源*allowedMethods允许的HTTP方法默认允许注解修饰方法对应的请求方式allowedHeaders允许的请求头列表默认允许所有请求头exposedHeaders允许前端JS读取的响应头不配置时只能读取默认缓存头allowCredentials是否允许携带Cookie等凭证默认falsemaxAge预检请求结果缓存时间单位秒默认1800秒参数看着多实际项目里最常配的就三个origins或originPatterns、allowCredentials、maxAge。如果前端需要带Cookie或带认证信息allowCredentials必须显式设为true并且这时候origins不能配*必须写具体地址。2.2 用它之前先确认你的模块边界CrossOrigin的优点是显而易见的简单、直观、局部生效改动范围可控。但它有个很实际的缺点只适合Controller数量少的小项目。我见过一个项目从一开始用注解后来Controller越来越多跨域配置散落在各个类上有的类忘记加前端一调用就报跨域。最后排查的时候得逐个类去翻维护成本极高。还有一次遇到的问题是某个类上用CrossOrigin(origins *)另一个类用了细粒度配置前端从不同入口进来后行为不一致非常难调试。所以我的建议是把CrossOrigin留给临时调试场景或者只给某个确实需要特殊跨域规则的独立接口用。如果你在做一个正式项目Controller数量超过五六个直接考虑下一种全局配置方案省心得多。还有一个注解用法上的坑Spring 5.3之后如果allowCredentials true且origins配置了*启动时可能会报IllegalArgumentException因为CORS规范不允许“允许所有源”和“携带凭证”同时出现。注解倒不是说完全不让配而是行为会比较绕。实际生产里带凭证的跨域请求源码必须写清楚别用通配符糊弄。3. 方案二实现WebMvcConfigurer全局统一配置的首选3.1 全局跨域规则的代码配置这是我在正式项目里用得最多的方式。实现WebMvcConfigurer接口重写addCorsMappings方法一个配置类管理所有跨域规则清晰且好维护。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里注意一个关键细节我用的是allowedOriginPatterns不是allowedOrigins。两者有什么区别allowedOrigins(*)表示允许所有源但和allowCredentials(true)一起用时会被Spring拒绝不能这样配。allowedOriginPatterns(*)支持正则模式匹配比如https://*.example.com:8080可以和allowCredentials(true)并存。如果你确定不需要带Cookie那把allowCredentials去掉直接用allowedOrigins(*)也没问题。但在前后端分离项目里Session登录、Token刷新之类的场景通常会用到Cookie所以我更推荐allowedOriginPatterns方案一次配好避免来回改。maxAge(3600)的意思是预检请求的结果可以缓存3600秒。这个参数值得重视预检请求本身是额外的网络开销缓存时间太短会频繁触发OPTIONS时间太长又可能在调试时看不到改动后的效果。开发环境我习惯设成60秒或干脆不设生产环境设3600秒性能与灵活性都能兼顾。3.2 与拦截器、Spring Security的配合顺序全局配置方案看起来简单但实际使用中很容易在“和其他组件配合”时栽跟头。先说拦截器。很多项目会加自定义拦截器做登录校验在preHandle里判断请求头里的Token没Token就返回401。问题来了预检请求OPTIONS通常不带业务Token你的拦截器一看没有Token直接拦截掉返回401。浏览器拿不到带CORS头的响应跨域依旧失败而且控制台报的错和你没配跨域时一模一样。正确的做法是在拦截器里放行OPTIONS请求或者更严谨一点直接让跨域相关的预检请求走此路。举个例子Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 其他登录校验逻辑... return true; }再说Spring Security。如果项目引入了Spring Security光写addCorsMappings还不行。因为Spring Security的过滤器链默认会拦截所有请求它自己有独立的CORS处理逻辑你必须显式开启Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(Customizer.withDefaults()) // 关键启用CORS .csrf(AbstractHttpConfigurer::disable) // 实际项目中按需决定 .authorizeHttpRequests(auth - auth .anyRequest().permitAll() ); return http.build(); }这里有个容易忽略的点http.cors()默认会查找名为corsConfigurationSource的Bean来加载配置。如果你想在Spring Security里做更精细的跨域控制可以显式定义一个这样的Bean。如果用不到addCorsMappings里配的规则也会被Security的默认CORS处理读取到前提是别忘了写.cors()这一行。4. 方案三CorsFilter过滤器精细控制跨域行为4.1 CorsFilter的注册方式与关键配置CorsFilter是Spring框架提供的跨域过滤器它直接实现了javax.servlet.Filter接口工作在Servlet容器层面。相比前两种方案过滤器方式最贴近底层覆盖面也最大能处理的路径规则更精细。先看标准写法Configuration public class CorsFilterConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }这种方案的本质是Spring会把这个Bean注册为一个Filter对所有进入应用的请求先做跨域规则判断符合条件的请求自动在响应里加好CORS头然后再继续交给Controller处理。有人可能会问能和WebMvcConfigurer方案一起用吗能用但我强烈不建议。两种方案同时生效时同一个响应可能被追加两次相同的响应头。有些浏览器会直接忽略重复头有些会报错显示冲突。而且配置重复会让排查问题的时候精神分裂你不知道到底是哪个配置在起作用。二选一选一个就好。4.2 实测中Filter最容易踩的坑场景一Filter顺序问题。如果你的项目里还有Spring SecurityCorsFilter需要排在Spring Security过滤器链之前否则Security会在跨域过滤器之前就把请求拦截了。用FilterRegistrationBean手动控制顺序是比较稳的做法Bean public FilterRegistrationBeanCorsFilter corsFilterRegistration(CorsFilter corsFilter) { FilterRegistrationBeanCorsFilter registration new FilterRegistrationBean(corsFilter); registration.setOrder(Ordered.HIGHEST_PRECEDENCE); return registration; }场景二重复加头。有些基础框架比如某些企业内部封装的Web框架自带了CORS处理你再注册一个CorsFilter两边都往响应里写头导致响应头里出现两个Access-Control-Allow-Origin。第一次遇到这种情况我排查了很久最后在Servlet的响应头里看到了重复行才反应过来。解决方法是先检查项目里有没有其他CORS相关配置确认没有再做Filter方案。场景三过滤器对路径的匹配粒度。UrlBasedCorsConfigurationSource支持按路径注册不同规则比如/api/**一套规则、/open/**另一套规则。但要注意路径匹配是基于Spring的AntPathMatcher规则的配置前先在纸上列出所有路径前缀避免写错路径导致规则没生效。我还遇到过一种情况路径一直匹配不上最后发现是请求带了上下文路径context path而配置里没写。这种情况把/api/**改成/**或者把context path拼进去就好了。5. 方案四Nginx反向代理生产环境最稳的手段5.1 在Nginx层解决跨域的配置模板如果你有Nginx在前面做反向代理那跨域问题其实可以在Nginx这一层解决后端代码一行不改。这也是很多生产环境的常见做法。思路是这样的前端访问http://www.example.comNginx把/api/开头的请求转发到后端的http://127.0.0.1:8080。因为浏览器看到的请求地址始终是www.example.com请求是“同源”的浏览器压根不会触发跨域检查。但有时候你会遇到不能完全靠反向路径解决的场景比如前端要直连一个第三方域名下的接口或者微服务网关对外暴露了多个域名。这时候就需要在Nginx配置里直接处理跨域响应头。下面是一份我在生产环境用过的配置模板server { listen 80; server_name api.example.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 Content-Type, Authorization; add_header Access-Control-Max-Age 3600; add_header Access-Control-Allow-Credentials true; return 204; } 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 Content-Type, Authorization; add_header Access-Control-Allow-Credentials true; proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有两个关键点。第一Access-Control-Allow-Origin我配置的是$http_origin而不是固定的域名。这样任何来源都会被原样回显适合开发阶段生产环境如果只允许特定域名需要改成固定域名或用if判断避免被任意网站抓取接口数据。第二OPTIONS请求直接返回204不带业务逻辑响应速度快也避免请求进到后端被业务代码处理。5.2 代理方案与网关方案怎么选如果你用的是Spring Cloud微服务架构还可以把跨域统一放在网关层做思路和Nginx类似。比如Spring Cloud Gateway里加一个CORS全局过滤器或者用spring.cloud.gateway.globalcors配置。那到底是Nginx还是网关我的经验是场景推荐方案原因单体Spring Boot应用Nginx配置简单部署架构清晰后端无感知微服务多实例集群网关如Spring Cloud Gateway所有跨域规则集中在网关服务治理更统一需要动态配置跨域白名单网关可以结合配置中心实时刷新Nginx要reload已有Nginx层且不想动Java代码Nginx改动最小上线风险低还有一个容易被忽略的细节如果Nginx已经用/api/前缀做了路径转发前端访问的是“同域”地址那后端根本不需要再配任何CORS配置。有些人既在Nginx配置了转发又在自己写的Spring Boot项目里加了一遍跨域配置结果响应头重复前端反而报错。代理层解决的是“伪装成同源”后端配置解决的是“允许跨域”两者方向不同别叠加着乱用。6. 跨域问题排查速查表与实战心得6.1 高频问题的定位清单跨域问题看起来报错都一样但真实原因千差万别。我把这几年遇到过的高频现象整理成了一张表现象可能原因解决方向浏览器报No Access-Control-Allow-Origin后端没有配置跨域或配置未生效检查CORS配置类是否被Spring扫描到OPTIONS请求返回404或空白拦截器或Spring Security拦截了预检请求放行OPTIONS或开启http.cors()带Cookie请求失败allowCredentials未开启或allowedOrigins用了*改成allowedOriginPatterns(*)allowCredentials(true)改了配置后浏览器仍报错预检结果被浏览器缓存硬刷新CtrlShiftR或用无痕窗口验证Nginx配了头但前端仍报错add_header继承问题或location未生效检查是否在if块里才加的头确认location匹配只有Chrome报错其他浏览器正常Chrome的安全策略更严格或扩展插件干扰无痕模式排除插件影响再检查请求Header本地用localhost访问成功用IP访问失败浏览器把localhost和127.0.0.1视为不同源检查allowedOriginPatterns是否把两种形式都包含进去6.2 我反复踩过的三个坑第一个坑Spring Security和WebMvcConfigurer同时配置了但没在Security里开启.cors()。那段时间前端一直报401后端日志是空的我一度以为是自己跨域规则写错了。后来发现是Security过滤器链压根没把CORS处理纳入流程加上.cors(Customizer.withDefaults())之后问题立刻消失。现在只要项目里同时出现Spring Security和跨域配置我第一反应就是检查Security里有没有开启CORS。第二个坑自定义拦截器拦截了OPTIONS预检请求。那是一个登录校验拦截器对所有请求先查Token。预检请求不带Token直接被拦截返回401整个过程耗时一下午排查。踩过这次坑之后我在所有拦拦截器里都习惯性地先判断请求方法如果是OPTIONS直接放行。第三个坑项目里既有老的JSONP跨域方案又手动加了CORS配置。JSONP只能发GET请求且对后端和前端都有侵入性当时维护起来极其痛苦。后来我把JSONP相关代码全部移除统一走CORS。其实JSONP是“没有CORS时代”的产物只支持GET也不能处理标准的错误状态码现在新项目完全不值得再考虑它。6.3 排查跨域问题的正确顺序最后整理一下我处理跨域问题的标准排查顺序先看浏览器Network面板确认报错请求是简单请求还是预检请求。看响应头里有没有Access-Control-Allow-Origin。如果没有问题大概率在后端没配置或配置没生效。看方法如果是OPTIONS先查它的响应状态码。404或401说明被拦截器或Security拦了。如果后端没有Nginx、网关等代理层直接检查Spring Boot配置如果有代理层先查代理配置再查后端。排除缓存干扰用无痕窗口或硬刷新再测一次。按这个顺序来绝大部分跨域问题都能在10分钟内定位到具体层级。我见过太多人一上来就改代码改到后面发现是Nginx配置少了几个响应头白折腾半天。7. 我实际项目中最常用的组合写到这里你可能会问我四种方案到底选哪个我把自己在真实项目里惯用的组合分享出来纯当参考。单体项目用Nginx反代后端不配任何跨域代码。前端请求同域浏览器不触发跨域检查省心。多模块或者微服务项目在网关层统一配CORS规则各业务服务保持干净不做跨域配置。理由是跨域规则属于入口治理的一部分散落到各个服务里会越管越乱。前后端分离开发阶段调试一些临时接口用CrossOrigin注解改接口灵活。但上线前会清理干净全部统一到全局方案或网关方案绝不留散装CORS。至于CorsFilter过滤器方案我通常在两种场景用一是无法加代理层、必须靠Java代码解决二是需要非常规的路径级跨域规则。除此之外用WebMvcConfigurer全局配置完全够用没必要为了秀技术多引入一个过滤器。还有一点个人体会跨域问题别只盯着“配置代码”看架构和部署方式对跨域的影响远大于代码本身。如果你的前后端始终走同一个域名入口通过代理转发那跨域问题根本不存在的可能性占八成。很多团队被跨域折磨本质上是因为部署架构没有统一入口每个环境都各配各的自然问题百出。先把部署架构理清再去纠结CORS头怎么写是真正少走弯路的办法。