ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

跨域解决方案精要:CORS、Nginx代理与前后端联调

跨域解决方案精要:CORS、Nginx代理与前后端联调 1. 跨域问题到底是怎么出现的先直接说结论调用后端接口报跨域不是你代码写得不对而是浏览器出于安全策略主动拦截了响应。后端接口本身可能返回了正常数据但浏览器拿到之后发现“这个响应和我当前页面不在同一个源”直接丢掉了然后在控制台给你抛一个红色的报错。这是Web开发里最容易让人血压升高的报错之一尤其是前后端分离的项目第一次联调接口的时候十有八九会撞上。要理解跨域先得知道浏览器的“同源策略”。所谓同源指的是协议Protocol、域名Host、端口Port三者完全一致。只要有一个不一样浏览器就会判定为跨域。举个例子你的前端页面跑在http://localhost:8080后端接口跑在http://localhost:9090端口不一样这就跨域了。更别说前端在https://admin.example.com后端在https://api.example.com域名不一样同样跨域。很多人第一次遇到这个问题时会觉得莫名其妙明明用Postman调接口返回数据很正常怎么放到页面上就报跨域原因就在于Postman这类HTTP客户端没有实现同源策略它发请求、接响应都是原原本本的而浏览器会多做一个“安全检查”。这个安全检查不是针对请求本身关键是针对响应。也就是说请求可能已经发出去了后端也可能处理了但浏览器不让页面拿到返回的数据。这种机制的初衷是保护用户如果没有同源策略你在A网站打开的页面就可以随意请求B网站的接口读取B网站的登录态和数据那整个互联网的账号体系就全乱套了。理解这一层之后你再看跨域解决方案思路就清晰了——所有方案的本质都是想办法让浏览器认为这个响应是“安全”的。在实际开发环境里最常见的情况就是前后端分离。前端工程用Vite或Webpack起一个本地开发服务器后端是独立的Spring Boot或者Nginx代理的网关服务两边端口不同、域名不同跨域几乎是必然发生的。生产环境虽然通常会通过Nginx做反向代理把接口和页面收敛到同一个域名下但开发阶段、以及接口被第三方系统直接调用的场景跨域还是绕不开。所以这个问题不是偶发的小毛病而是前后端开发者必须掌握的基础功。2. 主流的跨域解决方案选型网上关于跨域解决方案的文章很多但大多数只讲了怎么配置没讲为什么选这个方案。我先把主流的方案列一个对比表然后针对不同的项目场景说明怎么选。方案实现位置是否需要后端配合支持请求类型典型场景CORS跨域资源共享后端加响应头必须所有HTTP方法前后端分离、对外开放APINginx反向代理网关层配置不需要改代码所有HTTP方法生产环境收敛域名开发代理Vite/Webpack proxy前端开发服务器不需要所有HTTP方法本地联调JSONP前端动态script标签必须仅GET老系统兼容、第三方接口WebSocket协议天然不受同源限制不需要特殊处理双向通信实时消息推送Fiddler代理转发本地调试工具不需要改代码所有HTTP方法调试第三方接口、验证响应头这几种方案里CORS是目前最正规、最通用的做法也是后端开发者最常被问到的问题。它的核心思想是浏览器发请求时带上Origin头表示当前页面来源后端在响应里通过Access-Control-Allow-Origin这个响应头告诉浏览器“这个来源的页面可以拿我的数据”。浏览器拿到响应头一看哦允许的那就放行。就这么简单。Nginx反向代理则是一项绕过机制因为同源策略是浏览器限制的所以如果前端页面和接口在浏览器看来是同一个域名那就根本不存在跨域。做法是让Nginx监听某个独立域名比如https://api.example.com然后把所有进入这个域名的请求转发到真正的后端服务上。对外暴露的是统一的域名后端细节全被隐藏了。开发代理的原理和Nginx相似但只存在于本地开发环境。Vite、Webpack这类开发服务器内置了代理功能你请求/api开头的路径开发服务器会帮你转发到目标后端地址浏览器的视角里请求始终只发给了当前站点跨域自然不成立。JSONP是个老古董了它的原理是用script标签加载外部资源不受同源策略限制的特性把接口数据塞进一个JavaScript回调函数里返回。但这个方案只能支持GET请求而且有安全隐患新项目我基本不推荐。除非是接第三方老系统的数据人家只提供JSONP接口那没办法。Fiddler代理配置跨域这个方案有点特殊它属于“前端自己搞定”的一招。你本地装一个Fiddler设置一个代理转发规则把请求先打到本地再由Fiddler转发到目标后端并且在后端响应里追加CORS响应头。适合用来联调那种不允许你改代码的第三方接口或者后端同事暂时还没加上CORS响应头时临时救急用。整体选型建议如下开发阶段优先用Vite/Webpack代理不依赖后端任何改动生产环境用Nginx反向代理收敛域名接口要对第三方系统开放时老老实实让后端加CORS响应头。前两个是“绕”第三个是“允许”绕是开发效率的权宜之计允许才是对外服务的正牌方案。3. 后端接口加CORS响应头的完整实操后端加CORS响应头是最直接、最底层的解法。不管你用的什么语言、什么框架核心就是添加几个HTTP响应头。我先把标准响应头讲清楚再给出不同框架的具体配置方式。3.1 CORS响应头参数拆解后端接口要放行跨域请求至少需要设置以下响应头响应头作用示例值Access-Control-Allow-Origin允许哪个来源访问https://admin.example.com或*Access-Control-Allow-Methods允许哪些HTTP方法GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers允许请求携带哪些自定义头Content-Type, Authorization, X-Requested-WithAccess-Control-Allow-Credentials是否允许携带CookietrueAccess-Control-Max-Age预检请求结果的缓存时间3600第一个响应头是最核心的几乎决定了整个配置的对错。如果后端只写一个Access-Control-Allow-Origin: *意思是任意来源都能访问这个配置在接口完全是公开数据时可以用。但它在两种情况下会翻车一种是你需要携带Cookie。浏览器规定如果请求需要携带凭证CookieAccess-Control-Allow-Origin不能是*必须明确写成具体的来源域名同时Access-Control-Allow-Credentials必须设为true。用通配符时浏览器检查到Allow-Credentials: true和Allow-Origin: *同时存在会直接判定为非法配置拒绝放行。另一种是你需要区分不同环境的来源。比如说测试环境前端跑在http://test.example.com生产环境跑在https://admin.example.com如果后端写死一个来源那另一个环境又失灵了。这就要么在后端配置里做成动态读取请求的Origin头要么维护一个白名单列表。再强调一下Access-Control-Allow-Headers。前端发请求时如果带了诸如Authorization用来做登录态鉴权、Content-Type: application/json这类请求头浏览器在预检阶段就会问后端“我能不能带这些头”如果后端返回的Allow-Headers里没有包含对应的头浏览器一样拦截。很多项目配置了Allow-Origin和Allow-Methods但漏了Authorization结果前端明明带了token请求接口还是报跨域。这个坑我在联调时踩了好几次。3.2 预检请求OPTIONS必须处理跨域分两种情况简单请求和预检请求。简单请求是GET、POSTContent-Type限定为普通表单格式这类不会触发预检的请求。浏览器直接发送请求后端响应里带上CORS头就算完事。但一旦请求带了自定义头、或者Content-Type是application/json、或者用了PUT/DELETE方法浏览器会在正式请求之前先发一个OPTIONS请求来“探路”这就是预检请求。许多后端项目用Spring Security或者自定义拦截器默认拦下了OPTIONS请求结果预检返回403前端正式请求根本不存在发出去。报错信息往往是“CORS preflight response did not return HTTP status 200”之类的方向完全不对。正确的做法有两种一种是让OPTIONS请求直接放行不经过登录鉴权另一种是单独写一个拦截器只要请求方法是OPTIONS就直接返回200并且附带CORS响应头。不管用什么框架都要确保预检请求能拿到正确的CORS头这一步到位了主请求才会放行。3.3 各主流后端框架的配置方式拿Java的Spring Boot举例最省事的方式是写一个配置类实现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); } }这里有个细节allowCredentials(true)时方法名是allowedOriginPatterns而不是allowedOrigins。因为allowedOrigins(*)在SpringBoot 2.4之后的版本里和allowCredentials(true)会冲突直接用allowedOriginPatterns(*)更省心。如果项目里用了Spring Security光配置WebMvcConfigurer可能不够因为Spring Security的过滤器链执行顺序在MVC之前CORS头会被安全过滤器先拦掉。需要在Security配置里也加上CORS支持http.cors().and()然后在安全规则里对OPTIONS请求放行authorizeRequests() .antMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated()Node.js的Express服务用cors中间件几行就搞定const cors require(cors); app.use(cors({ origin: [https://admin.example.com, http://localhost:8080], methods: [GET, POST, PUT, DELETE], allowedHeaders: [Content-Type, Authorization], credentials: true, maxAge: 3600 }));如果你不想引中间件也可以在请求处理的最前面手工设置响应头原理是一模一样的。PHP后端设置响应头的方式比较直接在入口文件或者每个接口的公共逻辑里加上header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(200); exit(); }后面这个OPTIONS判断很重要。PHP项目很多老代码没有分层接口各自为战如果不在公共入口统一处理预检请求每个接口的跨域配置就会写得很分散排查起来非常痛苦。建议所有PHP项目把CORS配置抽到入口文件比如index.php或者公共中间层统一管理。3.4 网关层统一处理CORS才是长远之计如果你负责的项目是微服务架构或者后端有多个服务每个服务各配一套CORS响应头是个灾难。比如说你有用户服务、订单服务、支付服务前端一次请求可能要调动其中两三个如果各自配置不统一排查的时候一会儿好的、一会儿坏的非常难追。这种情况下建议在网关层统一处理。以Nginx为例在location级别加上CORS配置即可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, X-Requested-With; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Max-Age 3600; if ($request_method OPTIONS) { return 204; }这里用$http_origin而不是写死具体域名是为了动态回显请求来源方便后续扩展多个域名来源。但注意如果后端接口涉及重要数据配合Nginx的map指令做一个域名白名单校验更稳。比如固定只允许admin.example.com和app.example.com两个来源其他的一律拒绝。在网关层解决CORS还有个额外好处各个后端服务代码里完全不用关心跨域配置逻辑更干净这也是我对接多服务项目时最喜欢的方式。但要注意Nginx的add_header在PostAction阶段只在200和204等部分状态码上生效对于4xx、5xx错误响应默认不会带上CORS头如果需要错误响应也能被前端读取得用always参数add_header Access-Control-Allow-Origin $http_origin always;。这个细节容易被忽略前端拿到的错误信息往往是“Blocked by CORS policy”实际上后端已经返回了500但响应头里没有CORS头浏览器连错误详情都不给你看。4. 前端处理跨域的实操方案后端接口能在代码里改那怎么都好说。但很多时候你是在联调阶段或者其他团队的系统后端代码改不动或者改起来要排期这时候前端就得自己想办法绕过去。4.1 Vite和Webpack代理配置本地开发阶段我几乎无脑推荐用脚手架自带的代理功能。Vite项目的配置在vite.config.js里export default defineConfig({ server: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })这段配置的意思是页面里所有以/api开头的请求Vite开发服务器把它转发到http://localhost:9090。changeOrigin: true的作用是把请求头里的Host字段改成目标地址避免后端有些业务逻辑通过Host来校验来源。rewrite那一步是把路径中的/api前缀去掉再转发给后端这取决于后端接口到底带不带/api前缀。如果后端接口本身就是/api/user/list这种带前缀的那就不用rewrite如果后端接口是/user/list而前端约定统一加/api前缀方便代理识别就一定要rewrite。这个细节很容易栽跟头配置之后接口404多半就是这个原因。Webpack项目的配置逻辑完全一样位置在webpack.config.js的devServer.proxydevServer: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true, pathRewrite: { ^/api: } } } }用了代理之后前端代码里请求地址不要写全路径http://localhost:9090/user/list而是写相对路径/api/user/list。这样开发环境走代理生产环境拆掉代理逻辑或者换Nginx转发代码基本不用动。4.2 Fiddler代理配置跨域的实操细节Fiddler本质是个HTTP调试代理平时大家用它抓包看请求响应但它也能做请求转发——这就为“前端临时突破跨域”提供了一个思路。场景是这样的后端接口已经部署在测试环境但测试环境接口没有配CORS响应头你本地页面直接请求它必跨域。这时候Fiddler可以帮你做一个“响应头附加器”请求照常发到后端Fiddler在收到响应后往响应头里附加Access-Control-Allow-Origin: *这样浏览器认为自己收到了合法的跨域响应就不再拦截。在Fiddler里先开启代理监听菜单栏选择Tools - Options - Connections勾选Allow remote computers to connect记住默认代理端口是8888。然后把浏览器的代理设置为127.0.0.1:8888Chrome可以用SwitchyOmega扩展快速切换或者直接用--proxy-server127.0.0.1:8888启动参数。接下来在Fiddler的OnBeforeResponse脚本里添加一段逻辑在响应头里注入CORS字段。打开FiddlerScript Editor找到OnBeforeResponse函数里面对所有Content-Type以json开头的响应添加响应头if (oSession.ResponseHeader.Exists(Access-Control-Allow-Origin)) { oSession.ResponseHeader.Remove(Access-Control-Allow-Origin); } oSession.ResponseHeader.Add(Access-Control-Allow-Origin, *);保存之后Fiddler会根据脚本重写每次响应的CORS头浏览器就再也不报跨域了。这个做法的优点是完全不依赖后端配合适用于联调阶段。缺点也很明显Fiddler是个桌面工具只能在你本地用不能帮到团队其他人而且全局代理会影响所有网络请求调试其他项目的时候可能会有干扰。另外Fiddler修改的是本地调试代理上的响应应用在生产环境是不现实的——生产环境还是得靠真正的Nginx或CORS配置。所以我的定位是临时救急可以长期方案别这么做。4.3 JSONP在什么情况下值得用JSONP是零几年就出现的老方案到现在基本只活跃在老旧系统里了。它的实现思路是利用script标签加载资源不受同源策略限制这一点让后端返回一段JavaScript代码把数据包在回调函数里。前端代码如下function handleResponse(data) { console.log(data); } const script document.createElement(script); script.src http://api.example.com/geo/get?callbackhandleResponse; document.body.appendChild(script);后端识别到回调参数callbackhandleResponse之后返回的内容不是普通JSON而是handleResponse({name: 张三, id: 123})浏览器加载这个脚本等于执行了一个函数调用数据就进入前端的回调函数了。但在2024年的今天我不建议任何新项目主动选JSONP。首先它只支持GET想POST数据很难看其次它要求后端配合改造接口返回格式后端把数据拼进JS代码里安全隐患和调试难度都上升再加上现代浏览器对JSONP的跨域限制虽然没有取消但各大站点已经逐步禁用基于顶层导航的第三方脚本行为这种方案越来越不好使了。那什么时候值得用我遇到的真实场景是对接一个老旧的第三方支付平台对方只提供JSONP接口查询订单状态改接口得走版控流程。这种时候捏着鼻子也得用JSONP但用的时候要做好安全性剥离开的预期——任何用JSONP返回的数据都不要直接拼进DOM里防止XSS注入。5. 排查跨域问题的思路与常见坑跨域报错是前端最容易碰到、也最容易踩坑的一类问题。报错信息五花八门有说No Access-Control-Allow-Origin header is present的有说Response to preflight request doesnt pass access control check的还有说Credential is not supported if the CORS header Access-Control-Allow-Origin is *的。5.1 一套标准的排查思路碰到跨域报错先别慌按照这个顺序查第一步确认改动边界。问一下自己这个问题是我最近改动代码才出现的还是首次联调就遇到如果是首次联调多半是后端压根没配CORS响应头或者代理配置路径不对。如果是改代码后突然出现多半是后端某个响应头被改动了或者请求从简单请求变成了预检请求比如新增了自定义请求头。第二步打开DevTools的Network面板看请求到底发出去了没有以及响应是什么状态。如果请求显示为(cors blocked)说明请求连预检都没通过如果请求有响应但内容是红色报错说明请求发出去了但浏览器不允许读取。这是两个完全不同的方向前者重点查后端是否处理OPTIONS后者重点查响应头是否完整。第三步在Network里点开那个被拦截的请求看两个东西请求头里的Origin是什么响应头里有没有Access-Control-Allow-Origin。如果响应头完全没有CORS字段后端配置缺失直接找后端如果响应头里有CORS字段但和Origin不匹配是白名单配置不对也找后端如果请求压根在Network里没出现那就是前端代理配置没生效或者路径不对。第四步确认是不是Cookie引发的冲突。如果后端配置了Access-Control-Allow-Origin: *同时前端请求又设置了withCredentials: true浏览器会直接拒绝。原因前面讲过允许携带凭证时来源必须是具体域名不能用通配符。看到报错信息里有credential字样就往这个方向查。5.2 常见问题速查表报错现象可能性原因排查方向No Access-Control-Allow-Origin header is present后端完全没配置CORS头后端加响应头CORS preflight did not succeedOPTIONS请求被拦截或返回非2xx放行OPTIONS请求Credential is not supported if the CORS header is *Allow-Origin写死*且Allow-Credentials为true改为具体域名来源Request header field authorization is not allowedAllow-Headers没包含Authorization后端加Authorization前端代理配置后接口404路径rewrite规则写错检查/api前缀是否被正确替换代理后接口频繁断连changeOrigin未设true或代理目标地址不稳定检查目标域名是否加了http://协议用了Nginx转发还是报跨域add_header缺always参数错误响应没带CORS头加always5.3 几个我用经验换来的提醒第一Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true最好别同时用。就算某些情况下后端能配置出来浏览器也可能因为版本不同而表现得飘忽不定。如果接口需要携带登录态Cookie建议后端维护一个可信来源域名列表配置成精确域名。第二生产环境的跨域问题几乎别让代码改来解决。正确的顺序是先看Nginx层能不能通过add_header来解决再考虑后端代码加CORS头最后才考虑前端改代理。原因无他线上环境不可能为了调试而去刷新浏览器缓存、改代码配置靠网关层统一收敛是最可控的。第三开发环境用代理解决跨域时一定要确认代理配置是否真的生效。我见过很多开发者改了vite.config.js后只刷新页面没重启DevServer代理配置完全不生效白折腾了半小时。Vite的代理配置改动是需要重启服务才能生效的这是官方文档都写了但很少有人注意到的地方。第四调试跨域问题不要用Postman来判断“接口到底通不通”。Postman压根不执行浏览器同源策略接口通不通和浏览器能不能访问是两码事。我用过最舒服的调试方式就是直接看浏览器DevTools的Network面板接口请求和响应在那里展示得一清二楚比任何外部工具体验都好。6. 一次跨域问题的完整排查实录分享一个真实的案例。前阵子帮一个电商后台项目做权限模块的联调前端工程跑在http://localhost:3000后端服务跑在http://192.168.31.85:8080中间没有Nginx纯开发环境联调。前端同学说调用登录接口报跨域控制台报错信息是Access to XMLHttpRequest at http://192.168.31.85:8080/api/login from origin http://localhost:3000 has been blocked by CORS policy。我第一反应是后端没配置CORS。打开后端代码一看登录接口所在的Controller确实没加任何CORS相关注解不过鉴权过滤器里倒是有个统一的跨域处理逻辑但那个过滤器只对带有效token的接口生效登录接口走的是匿名认证链压根没经过过滤器。这个坑很有意思其他业务接口都有token过滤器会附加CORS头所以联调时发现其他接口都能通就登录接口跨域。前端同学一度以为是登录接口代码的问题还去核对了好半天请求参数格式完全跑偏。解决办法是在后端的Spring Security配置里增加一个独立的CORS配置源处理/api/login、/api/refresh-token这类匿名接口的OPTIONS预检和CORS头。这个案例很好地说明了一个道理跨域配置放在拦截器、过滤器的层级里时一定要确认不同请求路径、不同认证状态下的覆盖范围是否一致否则就是出现了“部分接口通部分接口不通”的诡异现象。后来又排查了一个更隐蔽的问题前端某个请求报跨域但Network面板里能看到响应头包含完整的CORS字段。后来仔细一看发现后端配置了Access-Control-Allow-Origin: *但前端发送前设置了withCredentials: true因为有些接口需要带Cookie做状态同步浏览器直接否决了。后端同事把Allow-Origin改成精确来源之后问题才消停。这两个案例让我更加确定跨域排查时最忌讳的是只盯着一行报错信息就下结论。报错信息只是浏览器给出的最终判断它背后的因果关系可能要沿着“请求头→响应头→拦截器/过滤器→全局配置”这条链路一层层剥开才能找到。最后给大家一个我常用的实操习惯在项目初期就让后端在Nginx层统一把CORS响应头配好前端开发环境用Vite代理生产环境走Nginx转发全部收口成一种方式。这样前后端各管一段接口联调时几乎没有多余的跨域噪音。如果你已经有一个线上项目在跑而且之前一直没管过跨域那建议先在网关层把CORS头加上这是改动成本最低、收益最大的操作。改完之后再用DevTools刷新页面验证几个关键接口只要返回头带上了Access-Control-Allow-Origin整个流程就顺了。
返回列表