
简介面向Spring Boot开发者的一份PDF技术资料专门讲解前后端分离场景下CORS跨域请求的配置方法。资料围绕两种主流实现方式展开一是基于CorsFilter过滤器通过重写doFilterInternal方法手动设置Access-Control-Allow-Origin、Access-Control-Allow-Credentials、Access-Control-Allow-Methods、Access-Control-Max-Age等响应头支持配置允许所有域名或指定来源并处理OPTIONS预检请求直接返回状态码确保跨域请求能正确通过二是通过CorsConfig配置类利用UrlBasedCorsConfigurationSource注册CorsConfiguration对象可灵活指定允许的来源域名、请求头、HTTP方法、过滤器执行顺序以及预检缓存时间并通过FilterRegistrationBean将过滤器注册到Spring容器。两种方案均附有可直接套用的代码示例同时梳理了CORS的基本概念、启用跨域后的安全性与灵活性优势以及在实际项目中如何按需求选择配置方式。资源为单个PDF文件压缩后大小仅34KB内容紧凑、结构清晰可帮助初中级Java Web开发者减少跨域配置踩坑快速完成Spring Boot接口联调。目前已有2835人学习下载适合正在处理前后端分离项目、需要对跨域问题快速定位与解决的开发者参考。1. Spring Boot 跨域报错两种配置方式先分清再动手前端页面跑在 localhost:4200Spring Boot 接口跑在 localhost:8080浏览器控制台一行 “has been blocked by CORS policy” 就把联调卡住了。此时后端日志里明明有请求记录前端却拿不到任何数据因为浏览器检查响应头Access-Control-Allow-Origin失败后会主动丢弃整个响应。Spring Boot 处理 CORS 主流有两条路一条是手写过滤器在doFilterInternal里手动设置响应头并放行 OPTIONS另一条是用CorsConfiguration定义规则交给 Spring 自带的CorsFilter执行。前者灵活但容易疏忽细节后者是官方推荐、配置集中更好维护。这篇文章把两条路的完整代码、参数含义和踩坑点全部过一遍适合正在被跨域问题折磨的 Spring Boot 开发者。2. 两个方案的选型边界同源策略与过滤器链在拦什么动手配 CORS 之前先把浏览器到底做了什么搞清楚否则你会在错误的方向上浪费大量时间。很多人看到跨域报错第一反应是“后端请求被拦截了”实际上请求早就打到服务器了Controller 也执行完了只是响应返回时浏览器发现缺少Access-Control-Allow-Origin直接把响应丢进了控制台。这就是为什么你在后端打日志能看到请求进来而在前端拿不到数据。判断请求是否真到了后端最简单的方法就是用 curl 直接打接口如果 curl 有数据而浏览器报 CORS 错那问题基本就定位在跨域配置上。2.1 同源策略与预检请求先弄明白浏览器到底拦了什么同源的定义是协议、域名、端口三者完全一致。http://localhost:4200请求http://localhost:8080端口不同属于跨域http 和 https 也视为跨域。浏览器默认不允许跨域读取响应这就是同源策略。它不是禁止请求发出而是禁止页面里的 JavaScript 读取跨域响应。CORS 机制做的就是让后端通过一组Access-Control-*响应头告诉浏览器“这个来源我可以信任你放行”。请求还分简单请求和预检请求。简单请求指 GET、POST、HEAD并且Content-Type为text/plain、multipart/form-data或application/x-www-form-urlencoded的请求。这类请求不会触发预检。但只要请求带了自定义头比如Authorization或者Content-Type设为application/json浏览器就会先发一个 OPTIONS 请求去问服务器“你允许我的来源、方法、自定义头吗”拿到明确允许的响应头后才发真正的业务请求。这个 OPTIONS 请求在 Network 面板里能看到如果状态不是 200或者响应头里没有Access-Control-Allow-Origin真实请求根本不会发出。下面这张表总结了常见触发预检的场景请求特征是否触发预检实际场景GET/POST 且无自定义头否普通数据加载带Authorization头是Token 认证接口Content-Type: application/json是前后端 JSON 交互使用 PUT、DELETE 方法是REST 接口理解预检机制后很多奇怪的现象都能解释通。比如接口明明写了CrossOrigin带Authorization的请求依然失败原因就是预检请求在到达 Spring MVC 前就被其他过滤器拦住了。排查跨域问题的第一步永远是打开 Network 面板看有没有 OPTIONS 请求、状态码是什么、响应头有没有Access-Control-*。2.2 手写 Filter 与 CorsConfig 的核心差别适用项目结构和维护成本两条路的本质区别很直接。方式一是自己写一个过滤器类在doFilterInternal方法里手动向 response 设置Access-Control-Allow-Origin、Access-Control-Allow-Methods这些响应头同时自己判断请求方法是否为 OPTIONS 并直接返回 200。一切都在你掌控里想对哪条路径加特殊头、想在哪个时机注入完全由代码决定。缺点是所有 CORS 头都靠手工维护少一个头、顺序错了、或者漏了 OPTIONS 的处理前端拿到的依旧是同样的报错。方式二是把 CORS 配置交给 Spring 的CorsConfiguration组件。你只需要声明允许哪些来源、哪些方法、哪些请求头然后通过UrlBasedCorsConfigurationSource绑定到 URL 路径Spring 自带的CorsFilter会在每个请求到来时自动检查并写入响应头自动处理 OPTIONS 预检。这是 Spring 官方文档推荐的做法配置集中在一个配置类里后续接手的人容易看懂。两种方式并不互斥但同一路径上同时注册会出现响应头互相覆盖的问题所以实际项目中选一种为主。选择哪一种主要看项目里有没有 Spring Security。老项目如果已经有一条复杂的过滤器链不想额外引入 Spring 的 CORS 组件方式一手写一个过滤器塞进去更直接新项目或标准 Spring Boot 工程我一般直接用方式二。方式二还可以配合 Spring Security 的http.cors()过滤器顺序的坑会少很多具体写法在后面的章节给出。另外要注意 Spring Boot 版本差异2.4 之后addAllowedOrigin(*)与allowCredentials(true)的组合会被禁用如果你从老项目升级上来用方式二反而比手写方式少踩一个版本坑。两种方式的横向对比直接看表对比项方式一手写 Filter方式二CorsConfig配置方式自己写响应头、自己处理 OPTIONSCorsConfiguration声明式配置预检处理手动判断 OPTIONS 并返回 200CorsFilter内部自动处理与 Spring Security 配合需要主动addFilterBefore可通过http.cors()组合可维护性较低头信息散落在代码中高配置集中在配置类灵活度高可精确控制每一行响应头中受 Spring 组件约束适用场景老项目、链路复杂、有特殊头需求新项目、需要多人维护的标准工程表里有一点要特别说明方式一虽然灵活但灵活意味着责任。所有 HTTP 头你得清楚是干什么的否则改了一个头以为没问题实际上前端又炸了。这里每个字段的具体含义下一章逐个拆开讲。3. 方式一手写自定义 CorsFilter逐头拆解 CORS 响应设置与过滤器注册方式一的代码量不大但每一行都有考察点。面试时问“项目里跨域怎么解决的”很多人张口就是“写了个过滤器加几个头”可问到Access-Control-Max-Age是什么、OPTIONS 为什么要单独处理往往答不上来。这一章把这些细节全部过一遍。3.1 自定义 CorsFilterdoFilterInternal 里每个响应头的实际含义先看完整实现public class CustomCorsFilter extends OncePerRequestFilter { private static final String ORIGIN Origin; Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String origin request.getHeader(ORIGIN); // 允许的来源优先把请求的 Origin 原样返回 if (origin ! null) { response.setHeader(Access-Control-Allow-Origin, origin); } // 允许携带 Cookie单点登录场景必须为 true response.setHeader(Access-Control-Allow-Credentials, true); // 允许的 HTTP 方法注意必须包含 OPTIONS response.setHeader(Access-Control-Allow-Methods, PUT, POST, GET, OPTIONS, DELETE); // 预检结果缓存时间单位秒3600 表示 1 小时内不再发 OPTIONS response.setHeader(Access-Control-Max-Age, 3600); // 允许的请求头前端带 token 时必须包含 authorization response.setHeader(Access-Control-Allow-Headers, content-type, authorization); // 预检请求直接返回 200不进入后续业务过滤器链 if (OPTIONS.equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return; } filterChain.doFilter(request, response); } }这段代码里我特意把类名写成CustomCorsFilter而不是原文中的CorsFilter目的是和 Spring 自带的org.springframework.web.filter.CorsFilter区分开。两个类同名时 import 很容易引错后续接手的人看半天不知道你用的到底是哪一个。如果项目里已经 import 了 Spring 的CorsFilter类名冲突会让你连编译都过不去改个名能省掉一系列麻烦。继承OncePerRequestFilter而不是直接实现Filter接口原因在于前者保证一个请求在过滤器链中只执行一次。在某些容器或代理转发场景下一个请求可能被多次转发普通Filter会被重复调用导致响应头被反复设置。虽然setHeader本身是覆盖语义但如果你在代码里做了累加逻辑就会出问题。项目里凡是需要写自定义业务过滤器我默认都继承它。逐头解释一下。Access-Control-Allow-Origin是最核心的头浏览器就靠它判断是否放行响应。这里没有写死*而是把请求头里的Origin原样回写原因是与Access-Control-Allow-Credentials同时使用时通配符会被浏览器拒绝具体在第 5 章展开。Access-Control-Allow-Credentials为 true 表示允许跨域请求携带 Cookie如果项目没用 Cookie这个头可以不设。Access-Control-Allow-Methods必须包含 OPTIONS否则预检一上来就被否了。Access-Control-Max-Age是预检结果的缓存时长3600 秒即 1 小时在这 1 小时内浏览器不会重复发送 OPTIONS能少一次往返。Access-Control-Allow-Headers里的content-type和authorization是实际开发中最常用的两个头前者对应 JSON 请求体后者对应 Token 认证漏掉任何一个对应的预检请求都会失败。3.2 注册进 Spring Boot普通 Web 环境与 Spring Security 环境两种放法过滤器类写好之后还要注册到 Spring 容器中。原文把Bean直接写在过滤器类内部这种写法在 Spring 里属于 lite mode确实能被处理但项目里可读性不好问题也不好排查。我建议单独建一个配置类放注册逻辑职责更清晰Configuration public class CorsFilterConfig { Bean public CustomCorsFilter customCorsFilter() { return new CustomCorsFilter(); } }如果项目是纯 Spring Boot Web没有引入 Spring Security注册到这里就够了。Spring Boot 会自动把容器里的Filter注册到内嵌 Tomcat 的过滤器链中顺序按 Bean 注册顺序排列。但这里有一个隐藏问题如果项目里其实有 Spring Security光有Bean还不够因为 Security 的过滤器链在 Servlet 层拦截请求你注册的自定义过滤器可能根本轮不到被调用。Spring Security 环境下需要显式把 CORS 过滤器加到安全过滤器链的指定位置Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Bean public CustomCorsFilter customCorsFilter() { return new CustomCorsFilter(); } Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(customCorsFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class) .headers() .cacheControl(); // 其他安全配置... } }代码的执行逻辑很清楚addFilterBefore把 CORS 过滤器放在UsernamePasswordAuthenticationFilter之前目的是让 OPTIONS 预检请求在进入鉴权逻辑之前就被直接 200 返回。如果顺序反了预检请求会被鉴权过滤器拦下返回 401 或 403浏览器拿不到允许跨域的响应头真实请求永远发不出去。两次addFilterBefore指向同一个位置时会按照添加顺序依次执行所以customCorsFilter先写入 CORS 头然后才轮到鉴权过滤器。对于只做跨域不做复杂安全控权的项目这两行顺序值得记一下这是我实践里调出来的坑。3.3 手写方式与 Spring 自带 CorsFilter 的同名陷阱与三个边界问题手写方式的第一个坑就是类名。Spring 自己有一个CorsFilter如果你把自定义类也命名为CorsFilter在 Spring Security 的http.cors()配置里会引发歧义甚至分不清到底是哪个过滤器在生效。建议直接换成CustomCorsFilter花几秒钟改个名后面能省掉一堆排查功夫。第二个边界是路径细分。上面的实现里所有路径都返回同一组响应头如果某个接口只想对特定来源开放就要在过滤器里补一层 origin 白名单判断。我一般维护一个SetString allowedOrigins判断当前请求的Origin是否在集合里不在就直接跳过setHeader让浏览器按无 CORS 头处理。这个逻辑放在doFilterInternal最前面30 行以内能写完。生产环境不建议用*一把梭一个原因是和allowCredentials冲突另一个原因是任何第三方来源都能调用你的接口爬虫连弯都不用绕。第三个边界是 OPTIONS 请求不能继续往后传递。代码里判断到 OPTIONS 就直接return不再调用filterChain.doFilter。如果漏掉这一句OPTIONS 会继续走后面所有过滤器Spring Security 里大概率 403Spring MVC 里会落到DispatcherServlet然后 404前端看到的都是同一个跨域错误。这里的原则很简单预检请求不是业务请求它在 CORS 过滤器这里就该被终结。4. 方式二用 CorsConfig 接管跨域配置参数、Order 与拦截路径一次讲清方式二是实际项目里我更常用的一条路。它不需要记住Access-Control-Allow-Headers具体有哪些请求头也不需要手动处理 OPTIONSSpring 的CorsFilter把这些脏活封装好了。你要做的只是把一个配置类写清楚告诉 Spring 允许谁、允许什么方法、允许什么头。4.1 CorsConfig 完整代码路径注册、来源白名单与请求头方法放通直接给配置类的完整代码Configuration public class CorsConfig { Bean public FilterRegistrationBean corsFilter() { // CORS 配置源按 URL 路径绑定配置 UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); CorsConfiguration config new CorsConfiguration(); // 允许携带 Cookie前后端分离登录态常用 config.setAllowCredentials(true); // 允许的来源多个域名可以重复调用不要和 * 混用 config.addAllowedOrigin(http://localhost:4200); config.addAllowedOrigin(https://admin.example.com); // 允许所有请求头和方法生产环境建议按需收紧 config.addAllowedHeader(*); config.addAllowedMethod(*); // 绑定到所有路径 source.registerCorsConfiguration(/**, config); // 用 FilterRegistrationBean 包装 Spring 自带的 CorsFilter FilterRegistrationBean bean new FilterRegistrationBean(new CorsFilter(source)); // 顺序设置到最前避免被其他过滤器抢先拦截 OPTIONS bean.setOrder(0); return bean; } }说明一下这里的关键点。UrlBasedCorsConfigurationSource的作用是按 URL 模式维护 CORS 配置集内部使用 Ant 风格的路径匹配/**表示所有路径。如果你的项目里有/api/**和/file/**两套不同的跨域策略可以注册两条匹配规则。这种设计比方式一的 if 判断更规范规则集中在一个对象里好维护。CorsConfiguration是核心参数对象。setAllowCredentials(true)对应方式一中的Access-Control-Allow-Credentials响应头。addAllowedOrigin可以调用多次每次加入一个白名单域名。这里注意Spring Boot 2.4 之后如果allowCredentials为 true 且addAllowedOrigin(*)Spring 会直接抛异常不允许这种存在安全漏洞的组合。需要全放开时用config.addAllowedOriginPattern(*)这是专门为与allowCredentials同时使用而设计的方法。addAllowedHeader和addAllowedMethod我这里放宽成*实际项目如果接口结构固定建议收窄能减少一些暴露面。4.2 FilterRegistrationBean 的 Order 与拦截路径setOrder(0) 为什么重要FilterRegistrationBean的作用是把 Spring 容器里的CorsFilter注册为 Servlet 容器级别的过滤器并同时设置拦截路径、执行顺序等参数。如果不经过这层包装CorsFilter只是一个普通 Bean不会被自动加进 Servlet 过滤器链。原文特意强调setOrder(0)这不是可有可无的代码它决定了 CORS 过滤器能否最先处理预检请求。Order 值越小越先执行0 在绝大多数应用的过滤器链里都能排到很前面。这里有个常见的理解偏差过滤器顺序不是只看你写的这个 0而是看整个链路中其他过滤器的 Order 值。如果项目里某个鉴权过滤器设置了Order(-100)它依然会排在 CORS 过滤器前面OPTIONS 预检请求会先被鉴权过滤器拦截并返回 401。所以setOrder(0)不一定百分百安全还要确认项目里没有更小的 Order 值或者干脆在 Spring Security 配置里再做一次显式放行作为双保险。这是当年项目里的一个血泪教训线上环境突然某条跨域请求失败排查半天发现新同事在另一个过滤器上设置了-1的 Order。拦截路径也值得留意。FilterRegistrationBean默认拦截所有路径但如果你在source.registerCorsConfiguration里只注册了/api/**非/api前缀的跨域请求到了CorsFilter时内部找不到匹配的 CORS 配置就不会写入任何响应头。这个行为很隐蔽因为接口本身能正常返回数据只是浏览器里永远看不到Allow-Origin头。遇到“部分跨域成功、部分跨域失败”的情况优先检查registerCorsConfiguration的路径和接口的实际前缀是否一致。4.3 两种方式的横向对比与不同项目的选择建议两种方式的代码都过完了我把它们在实际项目里的分量再对比一下。方式一适合对响应头有特殊要求、或者 Servlet 环境和 Spring MVC 混用的老项目。比如某些响应头需要动态变化或者想在一个 Filter 里同时处理 CORS 和其他鉴权逻辑手写 Filter 更方便。方式二适合绝大多数新搭建的 Spring Boot 工程配置集中、语义清晰交给初级开发也不容易写歪。方式二还有一条隐藏优势可以把CorsConfigurationSource单独暴露成 Bean这样 Spring Security 的http.cors()能直接使用连FilterRegistrationBean都省了。常见的做法是Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; }这段代码与方式二的区别在于返回的是CorsConfigurationSource不是FilterRegistrationBean。Spring Security 在调用http.cors()时会自动查询容器里是否存在名为corsConfigurationSource的 Bean有就用它初始化 CORS 过滤器。这样跨域配置被 Spring Security 和普通 MVC 请求共用两边的配置不会打架。如果你的项目是 Spring Boot 3Security 配置类的写法略有变化但这个 Bean 的用法基本一致。5. 避坑与排查CORS 配置中四个高频翻车现场与修复步骤跨域问题之所以让人头疼是因为同一个报错文案背后可能有四五种截然不同的原因。这一章用实际项目中踩过的高频问题做案例每种都按现象、原因、解决三步写清楚。5.1 后端配置了 CORS 头浏览器却仍然报 has been blocked by CORS policy现象后端确认配置已经加上响应头里也确实能看到Access-Control-Allow-Origin但浏览器依然报 has been blocked by CORS policy前端拿不到任何数据。原因绝大多数情况是配置重复了。比如一个项目里同时用了CrossOrigin注解、WebMvcConfigurer的addCorsMappings和自定义CorsFilter多个配置都往响应头里写值其中一个把另一个的配置覆盖掉。有些版本里响应头还会出现两个Access-Control-Allow-Origin浏览器直接判定非法并拒绝。另外如果请求是从明文 HTTP 的局域网 IP 地址发出的还会触发 the request client is not a secure context 这类报错这时已经不是简单的响应头配置问题而是浏览器安全上下文限制。localhost 不会有这个问题换成 IP 访问就会触发。解决统一入口。一个项目只保留一种 CORS 配置方式建议以配置类作为唯一入口把 Controller 上的CrossOrigin注解删掉避免多套配置运行时互相覆盖。排查时先用 Network 面板看响应头是否出现重复的Access-Control-Allow-Origin有重复就说明叠加了多个配置逐个处理后重试。5.2 OPTIONS 预检请求被 Spring Security 拦截真实请求始终发不出去现象前端带Authorization的请求一直失败Network 面板里能看到 OPTIONS 请求状态码是 401 或 403响应头里没有Access-Control-Allow-Origin浏览器连真实请求都不发。原因Spring Security 的过滤器链建立在 Servlet 过滤器之上默认所有进入应用的请求都会被安全过滤器先检查一遍。如果 CORS 过滤器没有被加进 Security 的过滤器链或者顺序排在鉴权过滤器之后OPTIONS 请求就会在没有任何 CORS 响应头的情况下被鉴权逻辑直接拒绝。手写过滤器时只往 Spring 容器注册了 Bean却没在 Security 配置里addFilterBefore这是最常见的原因。解决在 Security 配置里显式加入 CORS 过滤器或者调用http.cors()开启 Security 对 CORS 的内置支持同时放行 OPTIONS 请求http.cors().and() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated();5.3 addAllowedOrigin(*) 与 allowCredentials(true) 的组合被 Spring 直接拒绝现象按照网上一些旧教程写了config.addAllowedOrigin(*)同时又配上config.setAllowCredentials(true)应用启动后日志报出When allowCredentials is true, allowedOrigin cannot be *有的版本直接抛IllegalArgumentException应用根本起不来。原因浏览器对 CORS 的规范明确禁止 allow-credentials 为 true 时使用通配符来源。带凭证的请求如果来源是*等于任何网站都能携带 Cookie 访问安全性无法保证所以浏览器和 Spring 高版本都把这个组合掐断了。网上很多教程写得早教的还是旧写法Spring Boot 版本一高就翻车。解决改成addAllowedOriginPattern(*)。这是 Spring 5.3 之后专门提供的写法允许与allowCredentials共存运行时会把实际请求的Origin原样回写到响应头语义上是动态匹配而不是通配。如果能明确前端域名更稳妥的做法是逐个addAllowedOrigin列出具体域名。5.4 手写 Filter 未放行 OPTIONS 导致带自定义头的请求全部失败现象手写过滤器时只设置了响应头没有判断 OPTIONS 请求结果 GET、POST 这类简单请求正常只要带上content-type: application/json或者Authorization头就失败。原因这类请求会先触发预检。OPTIONS 请求进入过滤器后由于没有提前return它继续沿过滤器链往后走最终可能被后续业务过滤器拦截或者被 Spring MVC 当作找不到映射的请求返回 404。浏览器看到的还是跨域报错但实际问题是 OPTIONS 请求没有在 CORS 层被终结。解决在doFilterInternal里先判断请求方法OPTIONS 就设置状态码 200 然后直接return不调用filterChain.doFilter。一个快速的辅助判断在后端日志里搜 OPTIONS如果看到大量 OPTIONS 404 或 401 的记录基本就是踩了这个坑。使用方式二的 SpringCorsFilter则不需要关心这个分支内部已经处理好了。6. 用 curl 模拟 OPTIONS 预检请求快速验证跨域配置是否真的生效跨域问题联调最烦的是“前端说后端问题后端说响应头都加了”来回截图确认浪费时间。我后来养成的习惯是配置完先自己用 curl 模拟一遍浏览器的预检请求确认响应头和状态码没问题再让前端去测。这比在浏览器里一遍遍清缓存、刷新靠谱得多。6.1 curl 命令模拟预检请求并检查响应头模拟预检的核心是发一个带Origin和Access-Control-Request-*头的 OPTIONS 请求curl -i -X OPTIONS http://localhost:8080/api/user \ -H Origin: http://localhost:4200 \ -H Access-Control-Request-Method: GET \ -H Access-Control-Request-Headers: authorization, content-type-i参数让 curl 输出响应头。执行后重点看两处状态码是不是 200响应头里有没有Access-Control-Allow-Origin和Access-Control-Allow-Headers。如果状态码是 401、403 或 404说明请求在 CORS 过滤器处理之前或之后被其他逻辑拦了对照第 5 章的排查方向去修正。如果状态码是 200 但响应头里没有Access-Control-Allow-Origin多半是路径注册不匹配或过滤器未生效。6.2 浏览器 Network 面板配合验证两步定位问题在谁那一侧curl 验证通过后再打开浏览器按 F12 进入 Network 面板刷新页面后按类型筛选 XHR找到失败请求看响应头里是否包含允许跨域的字段。一个小技巧浏览器的 Console 里出现 CORS 报错时Network 里往往能看到两个请求一个是 OPTIONS一个是真实业务请求。如果 OPTIONS 存在且状态异常问题在后端预检处理如果 OPTIONS 正常、真实请求也发出去了但响应头缺少Allow-Origin问题多半是配置没有覆盖到这条真实请求的路径。如果项目里用了 Nginx 做反向代理还要顺带检查 Nginx 层是否把 OPTIONS 吞掉或者去掉了响应头。Nginx invalid cors request 这类日志一旦出现就要检查 location 配置里有没有显式处理 OPTIONS。代理层加的 CORS 头和后端加的 CORS 头同时存在时浏览器同样判定非法看到重复的Allow-Origin头先查 Nginx。跨域这件事本质不复杂全是一组响应头和请求方法组合的问题。我现在每接一个新项目都先把跨域配置方案确认清楚再让前端动手联调。配置完之后强制走一遍 curl 预检命令确认 200 和响应头齐全才放给前端去测。这个习惯帮我省掉了很多来回确认的时间也想让你省下这一步的麻烦希望帮到你。本文还有配套的精品资源点击获取