ARTICLE DETAIL

资讯详情

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

前后端联调跨域报错排查:同源策略、CORS、JSONP与代理转发全解析

前后端联调跨域报错排查:同源策略、CORS、JSONP与代理转发全解析 相信搞前后端联调的朋友都遇到过这种红彤彤的报错浏览器控制台里跳出“No Access-Control-Allow-Origin header is present”或者“Failed to load resource: Origin is not allowed by Access-Control-Allow-Origin”。接口用Postman测着一切正常参数没问题网络通着但一放到页面上发起请求就直接失败。这种时候十有八九就是跨域问题在中间捣鬼。其实跨域看着神秘背后的逻辑并不复杂。这篇文章我会从跨域的成因讲起把同源策略、CORS、JSONP、代理转发这些方案全部过一遍再附上几种场景下可以“直接抄”的配置步骤最后把我在排查时踩过的坑和总结的经验一起分享出来。不管你是刚入行的前端还是要跟前端对接口的后端读完这篇应该都能自己搞定跨域了。1. 先把跨域这件事说清楚1.1 同源策略浏览器里的社区安保要理解跨域必须得先知道为什么浏览器会拦你。这背后是网络安全里一个非常基础且重要的概念——同源策略Same-Origin Policy。什么叫“同源”简单来说就是协议、域名、端口这三个都一样。比如你正在访问https://www.example.com:443/api/data那么只有协议是https、域名是www.example.com、端口是443的请求才算是同源。三样里缺一样就算跨域。举个例子你现在在https://www.example.com这个页面上想要通过JavaScript请求http://api.other.com/data协议不同、域名不同、端口也都是默认的80/443这明显是跨域。又比如你从https://www.example.com请求https://www.example.com:8080/data端口变了也是跨域。同源策略就像小区里的安保系统。你住在这个小区里保安允许你在小区内部自由走动但你跑到隔壁小区去想进人家单元楼物业肯定要拦你。浏览器也是一样它默认不信任别的源的资源特别是当页面里的脚本想去读取另一个源的响应数据时会被狠狠拦截。注意一个关键细节请求其实发出去了服务器也收到了甚至还处理了但浏览器把响应的数据“扣押”了。这也是为什么很多后端看日志会非常疑惑——“明明我的接口通了怎么前端就说失败呢”原因就在这一层。明白这个点对后续排查跨域问题非常有帮助。1.2 什么样的请求算跨域在实际业务里跨域的场景很多我用一张表总结一下当前页面地址请求目标地址是否跨域原因https://a.com:443/xhttps://a.com:443/api/y否协议、域名、端口全相同http://a.com/xhttps://a.com/api是协议不同http和httpshttps://a.com/xhttps://b.com/api是域名不同https://a.com/xhttps://a.com:8443/api是端口不同https://a.com/xhttps://a.com/api否完全同源这里有个容易忽略的情况很多公司会有一个主域名下的多个子域名比如static.example.com和api.example.com。它们虽然共享一个主域example.com但子域名不同浏览器依然认为是跨域。解决子域名跨域可以用document.domain的方式来降级到同一个域但这法子比较古老只适合同主域下的页面和接口交互现在用得不多多数还是走CORS。另外还有一种非浏览器的跨域比如在网络设备中不同自治域AS之间通信也叫跨域例如SRv6 TE Policy跨域场景下的PE配置那个是路由层面的问题跟浏览器里的跨域不是一回事。数字电路里的“跨时钟域”又是另一个概念了。这些东西在专业领域各有各的技术细节别搞混了。2. 主流的跨域解决方案各有各的脾气2.1 后端开启CORS最正规的解法CORSCross-Origin Resource Sharing跨域资源共享是W3C推出的标准也是目前解决跨域问题的主流方案。它的思路很直接服务器在响应头中明确告诉浏览器“这个源我可以信任允许它读我的数据”。后端要做的就是给HTTP响应添加几个关键响应头Access-Control-Allow-Origin允许跨域访问的源可以指定为*表示任意源也可以指定具体的源比如https://foo.example。Access-Control-Allow-Methods允许的请求方法比如GET, POST, PUT, DELETE, OPTIONS。Access-Control-Allow-Headers允许的请求头比如Content-Type, Authorization。Access-Control-Allow-Credentials是否允许浏览器携带Cookie值只能是true。Access-Control-Max-Age预检请求OPTIONS结果的缓存时间。这个方案优点非常明显标准、安全、可控适合绝大多数Web应用。缺点是你得能改后端代码如果接口是第三方的或者说接口所属团队不归你管那这条路就走不通了。2.2 JSONP老古董但还能用JSONPJSON with Padding是解决跨域的一种“古早”方案原理非常巧妙利用script标签请求资源不受同源策略限制的特征。你可以在页面里动态创建一个script标签把接口地址塞进src属性里服务端返回的是一段JavaScript回调函数调用代码前端再定义好这个回调函数就能拿到数据。它的核心是服务端配合返回一段可执行的JS代码比如callback({“name”: “张三”})。前端脚本执行后相当于调用了callback函数参数就是你要的数据。这个方案的优点是不需要CORS支持老系统里兼容性好但缺点也一堆只支持GET请求无法支持POST、PUT这些方法而且安全性和错误处理都比较弱。JSONP目前主要用于一些老项目或者第三方平台在特殊场景下提供的兼容接口。2.3 代理转发把请求“骗”过浏览器代理转发的思路是让前端请求一个同源的接口然后由这个同源的中间层代理去请求真正的跨域接口拿到数据后再返回给前端。因为浏览器只认“同源请求”代理这层是服务端到服务端天然没有跨域限制。开发环境和生产环境都可以用。开发的时候我们通常用webpack的devServer.proxy配置一个代理生产环境则用Nginx的反向代理。这个方案的优点是你完全不需要动后端接口代码对于第三方接口或遗留系统特别友好。缺点是多了一层网络转发略微增加延迟同时代理服务器要承担流量转发注意配置好超时和缓存策略。2.4 改浏览器关掉安全限制只能本地调试有人可能会想既然跨域是浏览器安全策略搞的鬼那我把浏览器的安全策略关掉行不行我的回答是行但只限于本地调试。Chrome可以用一个特殊参数启动比如--disable-web-security --user-data-dir...这样浏览器就不会校验跨域了。不过千万不要在生产环境这么干。这相当于把小区保安撤了任何人都能随便进你家。你的应用裸奔在公共网络上用户的数据和隐私完全没法保证。我自己只用这个方法在本地连一下某个公共测试接口平时正经开发还是老老实实走代理。3. 实操记录三种场景从零配通3.1 场景一后端接口属于自己团队直接加CORS头如果接口是自己团队维护的最推荐直接后端开启CORS。我在实际工作中写了不少接口这个方案配置起来并不复杂。用Node.js的话如果你用的是Express框架可以装一个现成的中间件corsnpm install cors然后在你的入口文件里const express require(express); const cors require(cors); const app express(); // 允许所有源跨域访问 app.use(cors()); // 或者更细粒度地控制 // app.use(cors({ // origin: https://front.example.com, // methods: GET,POST,PUT,DELETE, // credentials: true // })); app.get(/api/data, (req, res) { res.json({ code: 0, data: ok }); }); app.listen(3000);如果你用的是Java Spring Boot加个注解更省事CrossOrigin(origins https://front.example.com) GetMapping(/api/data) public Result data() { // ... }注意Spring Boot的CrossOrigin默认允许所有源和方法如果你要带上Cookie要设置allowCredentials true并且origins必须指定具体的源不能是*。如果是PHP假设你用的是原生PHP在接口文件顶部加几行header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization); // 如果要带Cookie // header(Access-Control-Allow-Origin: https://front.example.com); // header(Access-Control-Allow-Credentials: true);有些PHP框架比如Laravel也有专门的中间件写个全局中间件把响应头统一带上就好。补充一个小细节Access-Control-Allow-Origin: *看起来挺省事但一旦你的请求需要携带Cookie浏览器会拒绝。实战中如果业务涉及登录态的传递建议把*改成具体的源同时开启Access-Control-Allow-Credentials: true。这也是配置CORS时最容易踩的坑。3.2 场景二后端接口是第三方的改不了用代理转发这类场景在对接上游供应商或者政府公共接口时特别常见。既然改不了别人的接口我们就在自己的“地盘”上加一层代理。开发环境Webpack Dev Server以React或Vue项目为例你可以在项目的webpack.config.js里配置module.exports { // ... devServer: { proxy: { /api: { target: https://third-party.example.com, changeOrigin: true, pathRewrite: { ^/api: } } } } }这里的意思很明白凡是浏览器发往/api开头的请求都会被webpack dev server代理到https://third-party.example.com去。changeOrigin: true是为了让请求头里的Host变成目标域名免得部分第三方服务器做域名校验时拒绝。pathRewrite则是把路径里的/api前缀去掉或者改成别的路径具体看要求。在Vite环境中配置在vite.config.js里用server.proxy语法和webpack类似。生产环境Nginx一般前端项目打包后会放在Nginx上这时候可以用Nginx反向代理来转发请求location /api/ { proxy_pass https://third-party.example.com/; proxy_set_header Host third-party.example.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }需要注意proxy_pass最后带了/这个非常关键。如果proxy_pass是https://third-party.example.com没有斜杠而location是/api/那么请求/api/user会被转发成https://third-party.example.com/api/user如果proxy_pass是https://third-party.example.com/有斜杠请求/api/user则会被转发为https://third-party.example.com/user。很多人就是因为漏了这个斜杠导致接口路径不对。配置完后记得执行nginx -t检查语法再nginx -s reload生效。3.3 场景三用的还是老掉牙的GET接口JSONP一把梭说实话现在的新项目里已经很少看到JSONP了但偶尔对接一些老系统对方就是只支持JSONP你也没办法。我之前对接一个合作商的单点登录接口对方给的文档明确写了“回调格式采用JSONP”只好老老实实实现。实现JSONP基本思路是这样的前端动态创建一个script标签把回调函数名通过callback参数传给后端后端返回一段JS代码。假设后端地址是https://api.example.com/getUser前端可以这样做script function handleUser(data) { console.log(拿到用户数据, data); } var script document.createElement(script); script.src https://api.example.com/getUser?callbackhandleUser; document.body.appendChild(script); /script后端PHP代码大致是$callback $_GET[callback]; $data [name 张三, age 18]; echo $callback . ( . json_encode($data) . );返回的内容就是handleUser({name:张三,age:18})。浏览器加载这段脚本就等于执行了这个函数于是数据就传回前端了。如果你用的是jQuery可以直接这么写$.ajax({ url: https://api.example.com/getUser, dataType: jsonp, jsonp: callback, success: function (data) { console.log(data); } });jQuery会自动帮你生成随机函数名、创建script标签、再处理回调后的清理。很方便但一定要记得这种方式只用GET请求而且参数会暴露在URL里不适合传输敏感数据。3.4 调试利器Fiddler代理配置跨域开发中偶尔会遇到一种情况后端接口已经配了CORS但不同浏览器或不同网络环境下表现不一致。为了快速定位我经常用Fiddler来临时修改响应头模拟CORS放行看看是不是响应头真的缺失了。Fiddler是一种抓包调试工具它本身可以作为一个反向代理修改请求头和响应头。配置方法很简单打开Fiddler按Ctrl R打开FiddlerScript在OnBeforeResponse方法里加一段代码if (oSession.HostnameIs(api.example.com)) { oSession.oResponse.headers.Add(Access-Control-Allow-Origin, *); oSession.oResponse.headers.Add(Access-Control-Allow-Methods, GET, POST, OPTIONS); oSession.oResponse.headers.Add(Access-Control-Allow-Headers, Content-Type, Authorization); }保存脚本然后设置前端代理指向Fiddler默认端口8888。这样即使后端没返回CORS头Fiddler也会帮你加上。这个方法主要用于本地联调尤其是后端说“我加了CORS”但前端还是报错的时候用来快速判断是浏览器缓存问题还是后端真的没加。注意Fiddler只适用于本机开发调试不要在生产环境开代理也不要用来做任何绕过网络限制的事情那是完全不同的概念。4. 常见坑位与排查手册4.1 预检请求OPTIONS为什么我们经常看到发两次请求很多新手在调试时发现某个跨域请求Network面板里出现了两条记录一条是OPTIONS一条是真正的POST或GET会非常困惑。其实这是CORS机制的预检请求。当请求不是“简单请求”时浏览器会先发送一个OPTIONS请求问服务器“我打算这样跨域请求你你允许吗”服务器如果返回正确的CORS响应头浏览器才真正发起业务请求。那什么样的请求算简单请求满足以下条件基本都是简单请求请求方法只能是 GET、HEAD 或 POST请求头只能包含Accept、Content-Type等几个简单头Content-Type只能是application/x-www-form-urlencoded、multipart/form-data或text/plain。如果你用了application/json作为Content-Type或者带了自定义Header比如Authorization那就属于非简单请求会触发预检。处理方式就是后端对OPTIONS请求做出响应可以返回2xx状态码并带上允许跨域的头部然后不再继续处理业务逻辑。如果后端不处理OPTIONS浏览器就拿不到预检通过的响应后续的GET/POST请求就别想发出去了。在Nginx代理场景下如果担心预检请求打到后端太频繁可以这样配置在Nginx层直接返回location /api/ { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin $http_origin; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization, X-Requested-With; add_header Access-Control-Max-Age 86400; return 204; } proxy_pass http://backend/; }这样OPTIONS请求在Nginx这一层就被处理掉了不会转发给后端能省不少事。4.2 带Cookie的请求为什么Access-Control-Allow-Origin设成*反而坏了这个问题我在团队里至少被问过十次为什么我后端明明配了Access-Control-Allow-Origin: *请求也带上Cookie了前端还是报错因为浏览器规定当你的请求设置了withCredentials: true也就是要携带Cookie时Access-Control-Allow-Origin不能为*必须是具体的源地址。同时服务端还要设置Access-Control-Allow-Credentials: true。你想想如果任意一个网站都能用你的Cookie跨域访问你的银行那多危险。浏览器不允许这种“任意源携带认证”的组合是为了防止潜在的安全漏洞。所以正确做法是后端判断一下请求的Origin动态返回允许的来源并且设置Access-Control-Allow-Credentials: true。Nginx里可以通过$http_origin动态回显Origin来解决add_header Access-Control-Allow-Origin $http_origin; add_header Access-Control-Allow-Credentials true;但这样做也要注意动态回显Origin会在某些配置下允许任意来源携带Cookie。如果生产环境这么做建议加上域名白名单判断。4.3 配置都对了还是报错先从浏览器Console和Network里找线索跨域问题最烦的一点是错误提示往往藏得深。我的建议是不要光盯着Console里那行红色报错一定要结合Network面板综合判断。先看Network面板里那条红色的请求点开它的响应头Response Headers看看里面有没有Access-Control-Allow-Origin。如果没有说明后端没加CORS响应头如果有那就要检查这个值和请求头里的Origin是否匹配。再看请求头Request Headers看看Origin字段是什么。比如请求头里是Origin: https://front.example.com响应头却是Access-Control-Allow-Origin: *这通常没问题但如果响应头是Access-Control-Allow-Origin: https://other.example.com不匹配浏览器照样拦。最后建议关闭一遍浏览器缓存或者用无痕模式再试一次。我之前遇到过一个情况配置改完之后浏览器依然旧错最后发现是浏览器缓存了旧响应强制刷新才生效。4.4 补充跨域概念不只浏览器有前面讲的基本都是Web浏览器的跨域。实际上“跨域”这个说法在不同技术领域都有出现只是含义完全不同。比如在网络设备领域中兴M6000-S设备在SRv6 TE Policy跨域场景中作为PEProvider Edge的配置说的是不同SDN域或不同自治域之间的路由打通需要配置BGP-LS、SRv6 Policy等这个“跨域”是网络层的概念跟浏览器跨域八竿子打不着。数字芯片设计里也有一个“跨时钟域”Clock Domain CrossingCDC的概念指的是信号从一个时钟域传递到另一个时钟域之间的同步问题通常要用两级寄存器同步、异步FIFO或握手协议来处理防止亚稳态。我为什么提这两个例子因为在不同技术圈子里搜“跨域”出来的内容可能完全不一样。如果你百度“跨域”出来的八成是前端CORS但你在网络工程师的群里问“跨域”大佬们可能跟你聊BGP路由反射器在硬件工程师那边又会聊异步FIFO。写这篇博文时我特意强调我们讨论的是“调用后端接口报跨域”这个场景就是希望读者不要搞混。最后分享一点我自己的扛坑经验做了这么多年开发我越来越觉得跨域问题并不难难的是在复杂环境下快速定位到底是哪一层出了问题。我个人遇到跨域报错时会按照这个顺序快速分类先看请求有没有发出去。如果Network面板里该请求的状态是 canceled 或者说没有记录那可能是浏览器拦截如果是failed或红色报错看响应头缺什么、响应体是什么。我见过很多同事一看到跨域就急着让后端改配置结果发现其实是请求协议混了页面是http被强制跳https接口还是http的非安全请求或者是请求路径写错导致404被浏览器判定为跨域拦截。另一个经验之谈无论是CORS也好代理也好尽量在开发初期就确定跨域方案不要等到联调阶段再来救火。如果你的项目前后端分离让前端在开发环境跑webpack代理后端在测试环境配好CORS生产环境用Nginx统一管理跨域。这一套流程理顺了后面基本不会因为这些破事加班。跨域问题本质上就是浏览器和服务器之间的一场“信任确认”。理解了信任规则配置无非就是几行代码。希望这篇内容能让你下次再看到跨域报错时心里不慌甚至不用查资料直接啪一下把问题搞定。
返回列表