资讯动态

跨域问题全解析:从同源策略到Nginx反向代理实战

发布时间:2026/10/1 14:34:30 来源:尧图企业网站定制
1. 被浏览器拦下来那一刻先别急着骂前端我在做项目联调的时候几乎每隔一阵就会遇到同一个人在群里喊一嗓子接口通了控制台全是红色的报错Access-Control-Allow-Origin什么的这到底是谁的问题一开始大家还会耐心解释后来就形成条件反射了先打开Network面板看一眼请求到底发出去了没有再决定是找后端还是找前端。说实在的跨域这个问题是前后端分离以后绕不开的一道坎。只要你的页面跑在a.com接口却在b.com或者端口不一样浏览器就会启动一套保护机制把请求拦下来。这套机制就是我们常说的浏览器的同源策略英文叫Same-Origin Policy。这篇文章我想从实际踩坑的角度把同源限制到底限制了什么、跨域问题的几种常规解法、以及我用nginx反向代理解决跨域的一套完整配置从头到尾捋一遍。文章会偏向实战既写给被跨域折磨的新手也写给想确认自己配置有没有踩坑的工程师。在往下看之前先把结论摆在这里绝大多数跨域问题都不是后端接口不通也不是前端代码写错而是浏览器基于同源策略把响应给拦了。理解了这一点你就能根据不同的场景在JSONP、CORS、nginx代理之间选出正确的方案。1.1 同源的本质协议、域名、端口三者全对上才算自己人同源里说的源指的是三样东西的组合协议、域名、端口。协议就是http、https域名就是example.com这种东西端口就是8080、3000这种。只要三样里有任何一个不一样页面和接口就不算同源浏览器的同源策略就会生效。打个比方。你在自己家页面源里朝隔壁邻居接口源喊话浏览器相当于小区物业。物业规定只有本小区的住户之间才能直接递东西你要想跟隔壁小区的人递东西必须经过门卫检查。同源策略就是那个本小区的边界。我列几个典型的非同源情况大家可以对号入座页面在http://localhost:8080接口在http://localhost:3000——协议一样域名一样但端口不同非同源。页面在http://a.com接口在http://b.com——域名不同非同源。页面在http://a.com接口在https://a.com——协议不同非同源。注意很多人容易忽略端口。本地开发时前端工程用webpack dev server跑在8080后端服务跑在3000这俩就是典型的跨域。Node接口明明在Postman里调得好好的一放到浏览器里就报错原因就在这里。1.2 为什么浏览器非要搞一个同源策略同源策略不是某些浏览器厂商抽风做的限制而是Web安全模型的基石。它存在的核心目的是防止一个网站去读取另一个网站的敏感数据。举个例子假设你登录了网银浏览器里存着网银的Cookie。这时候你打开了另一个恶意网站这个网站的脚本想偷偷向网银发一个请求读取你的账户信息。如果没有同源策略这个请求就能带着你的Cookie发出去恶意网站也就能读到响应内容你的账户就危险了。同源策略限制的是跨源读取和跨源操作。比如跨域的Ajax请求可以发出但浏览器会拦截响应脚本读不到返回数据。跨域的DOM访问会被禁止比如a.com的脚本不能随意操作b.com页面里的元素。Cookie、LocalStorage、IndexedDB这些浏览器存储默认也只允许同源访问。很多人在控制台看到的报错其实是这样的请求其实已经发出去了后端也处理了甚至都返回200了但浏览器把响应藏了起来不让你拿到。这是同源策略里最让人迷惑的地方——看着像没通实际上后端日志里明明有记录。理解这一点是解决跨域问题的第一步。1.3 被同源策略拦住的常见场景我结合日常开发和维护项目的经验总结一下跨域问题的高发场景前后端分离开发前端静态资源部署在Nginx或Tomcat后端接口是另一台服务器的API服务这几乎是默认跨域。本地联调前端本地跑着Node服务后端在测试环境本地页面访问测试环境接口跨域。单点登录或多域名体系a.com的页面要携带b.com的认证信息跨域。第三方API接入一些支付、地图、天气类的公开接口如果对方没有配置允许跨域直接调就会报错。这些场景里有的是可以通过后端改配置解决的比如加CORS响应头有的是通过前端换方案解决的比如JSONP有的是通过在中间架一层代理解决的比如nginx反向代理。下面我逐个说。2. 跨域的常规解法JSONP、CORS、代理各自的边界在哪里搞清楚同源策略拦的是什么之后解决方案就很好理解了。思路无非三种绕开浏览器的拦截逻辑、让浏览器认可这次跨域请求、或者干脆让请求看起来同源。2.1 JSONP借script标签打擦边球的经典方案JSONP全称是JSON with Padding是我早期做前端时经常用的一种方案。它的原理不复杂src引用外部脚本不受同源策略限制所以前端可以动态创建一个script标签把接口地址放进src里。假设接口返回的是一段JSON数据JSONP的做法是让后端把数据包在一个函数调用里面返回比如callback({name: zhangsan, age: 18});前端预先定义好callback函数然后把script标签插入页面function callback(data) { console.log(data); } const script document.createElement(script); script.src http//api.example.com/user?id1callbackcallback; document.body.appendChild(script);服务端看到callback参数后把数据包装成callback({...})的格式返回。脚本加载完成前端定义好的callback函数就被执行了数据也就拿到了。JSONP看起来有点取巧实际也真的有场景值回票价。我做过一个老系统的对接那个后端服务是Java写的升级成本很高对方死活不想动接口只给我留了一个能在返回数据里包一层函数调用的口子最后就是用JSONP解决的。另一个常见场景是偏老式的一些公开数据接口——比如某些券商行情接口——老协议里天然支持JSONP的返回格式。但JSONP的局限性非常明显它只支持GET请求。POST、PUT、DELETE这些方法统统用不了。此外JSONP没有标准的错误处理机制接口超时或返回异常时前端很难精准捕获。还有一个隐患JSONP接口可以被别的网站随意拿script标签去调用如果接口里涉及敏感操作安全风险比较大。所以我的判断是JSONP更适合用在简单查询类和数据爬取类的场景。现在后端基本都能改CORS头的新项目里我不太推荐上来就用JSONP除非对方接口真的不支持CORS。2.2 CORS让浏览器放行的标准姿势CORS全称是Cross-Origin Resource Sharing翻译过来就是跨源资源共享。它的思路跟JSONP完全相反不是绕开同源策略而是让后端通过响应头告诉浏览器这次跨域请求我允许你放行。后端在响应里加几个关键响应头最常见的组合是Access-Control-Allow-Origin指定允许访问的源。可以是具体域名也可以是星号表示允许所有来源。Access-Control-Allow-Methods允许的请求方法比如GET、POST、PUT、DELETE。Access-Control-Allow-Headers允许携带的请求头比如Content-Type、Authorization、X-Requested-With。Access-Control-Allow-Credentials是否允许携带Cookie设置为true时Allow-Origin就不能用星号必须指向具体域名。我拿Node.js的Express举个例子app.use(function (req, res, next) { res.setHeader(Access-Control-Allow-Origin, http://front.example.com); res.setHeader(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type,Authorization); next(); });前端在发起跨域请求时属于简单请求的话浏览器直接发真实请求后端响应带CORS头就行。如果是复杂请求比如带Content-Type: application/json、带自定义头、或者用PUT/DELETE浏览器会先发一个OPTIONS预检请求问后端这个跨域请求能放行吗后端返回上面那串CORS头浏览器才正式发真实请求。这里有个坑很多后端同学加CORS只看GET请求通没通忘了处理OPTIONS。结果前端明明预检失败控制台也不直接告诉你预检没过而是给你抛一个CORS错误排查起来总感觉隔着一层纱。正确的做法是后端对OPTIONS请求直接返回200带上CORS响应头即可。CORS是目前最正规、最普适的跨域解决方案。只要你有后端接口的修改权限强烈建议优先考虑CORS。但如果接口属于第三方你没有权限改对方的响应头或者后端同事没法配合改那就需要走代理方案了。2.3 代理转发绕开浏览器让服务端去沟通代理方案的思路更直接既然浏览器不允许页面跨域访问接口那我不让浏览器直接访问接口转而访问一个同源的地址再由这个地址的服务器去转发请求给真正的接口服务器。服务器和服务器之间通信不受同源策略限制所以请求可以正常到达并拿到响应。整个过程对浏览器来说页面请求的始终是同一个源自然就不存在跨域问题了。这里面最常见的落地工具就是nginx。nginx是一款高性能的Web服务器和反向代理服务器它最常见的用途除了托管静态文件外就是作为反向代理把请求转发给后端服务。配好之后前端的请求发向自己的域名同源路径nginx在中间悄悄把请求转到真实接口地址拿到结果再返回给前端。整个过程完全透明前端和后端都不需要大改。我参与过的项目里最常见的一种部署方式是nginx前端托管的方案。静态资源由nginx直接serving动态接口请求走nginx代理转发。这两个层级叠加页面和接口在浏览器眼里就是同一个域名、同一个端口、同一个协议于是同源策略压根不会被触发。3. nginx反向代理解决跨域的实战配置nginx代理可以说是前端独立解决跨域问题的一把利器因为它不需要后端配合也不需要改业务代码只要你在nginx配置里把规则写好前端和接口的源就被对齐了。3.1 为什么选nginx而不是其他方案有人会问代理工具那么多比如webpack devServer的proxy、Charles、Fiddler、Whistle为什么我特别推荐nginx第一webpack devServer的proxy只在本地开发环境生效。前端工程一旦构建上线部署到测试或生产环境后代理配置就不再起作用生产环境的跨域问题还得另想办法。nginx则不同它直接部署在服务器上开发、测试、生产都能用一套配置逻辑。第二Charles和Fiddler这类抓包工具本质上是在你本机做代理转发适合调试阶段不适合作为正式环境方案。你总不能让用户在他电脑里装个Charles去请求你的接口吧。第三nginx解决了静态资源和接口请求分属不同服务器这个典型场景。直接用代理层把它们在同一个域名下统一暴露出去从根源上避免跨域。以我的经验nginx处理跨域的思路其实是把前端和接口不同源这个事实通过中间层伪装成同源。原本前端访问http://www.example.com接口在http://api.example.com。通过nginx代理前端只访问http://www.example.com遇到/api/的前缀时nginx把它转到http://api.example.com去。3.2 最基础的跨域代理配置先看一个最简配置。假设前端页面部署在80端口后端接口跑在服务器上的8080端口我要让前端所有以/api/开头的请求都被转发到8080。server { listen 80; server_name www.example.com; root /usr/share/nginx/html; location / { try_files $uri $uri/ /index.html; } location /api/ { 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; } }这里最关键的是location /api/这个块。请求http://www.example.com/api/user/list时nginx会命中这个location然后把请求转发到http://127.0.0.1:8080/api/user/list。注意proxy_pass后面如果只是写了服务器地址没有加路径那么原始请求的完整路径会被原样带到后端。也就是说后端接口接收到的是/api/user/list不是/user/list。这个行为直接影响后面路径重写的配置方式。proxy_set_header这几行是设置转发的请求头。Host改成$host表示让后端看到的Host还是前端页面的域名而不是127.0.0.1:8080。X-Real-IP和X-Forwarded-For用于把客户端真实IP传给后端方便后端做日志和审计。配置改完后执行nginx -t检查语法然后nginx -s reload重载配置基本就能生效了。这时候前端的Ajax地址从http://api.example.com改成相对路径/api/user/list即可。相对路径有一个好处是无论你用http还是https访问页面代理请求都会自动用同样的协议避免出现页面是https请求却发到http的接口导致混合内容被浏览器拦截这种问题。3.3 带路径重写和Cookie传递的进阶配置上面那个基础配置胜在简单但真实项目里经常会遇到路径需要对不上号的情况。后端接口的正式路径只有/user/list并没有/api前缀。如果仍然用基础配置nginx把请求转到后端时就会带着/api后端找不到这个路由直接404。解决办法是用nginx的rewrite指令在转发时把前缀去掉。location /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8080; }rewrite正则的含义是匹配以/api/开头的路径把/api之后的内容捕获下来替换成以/开头的路径。比如/api/user/list被重写成/user/list然后再转发给后端。这样后端接口定义不需要做任何调整。另一个经常踩坑的地方是Cookie传递。如果你要求跨域请求携带Cookie比如用户登录状态那么必须确保两个条件后端接口允许携带Cookie也就是Access-Control-Allow-Credentials要设置而且Access-Control-Allow-Origin不能是星号。前端Ajax需要设置withCredentials: true。但走nginx代理后其实可以灵活一些。Cookie通常绑定在域名上如果你让前端请求走代理cookie的domain是www.example.com后端看到的是代理转发的请求那后端种Cookie时要注意domain的设置。实践中我有时候会在proxy_pass转发时带上Cookie请求头proxy_set_header Cookie $http_cookie;这行会把前端请求里的Cookie原样转发给后端。后端如果需要读取用户的登录态就能根据这个Cookie去解析。但要注意Cookie的domain和path如果和后端预期不一致会种不上。更稳妥的做法是让后端把Cookie的domain设置成代理的域名或者干脆用token机制替代Cookie不在这块过多纠结。我还想提一个进阶场景WebSocket代理。现在不少功能用WebSocket做实时通信比如在线客服、消息推送、协同编辑。WebSocket握手阶段同样受同源策略限制。nginx可以对WebSocket做代理转发需要额外设置两个请求头location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }不加这两行WebSocket连接在nginx这一层就会断掉客户端会一直处于连接失败的状态。4. 踩坑实录与排错链路代理配好了接口还是报跨域错光说正常配置还不够我把平时被问得最多的场景拿出来完整走一遍排查过程。很多同学是照着配置贴上去的结果接口依然报跨域就开始怀疑nginx没生效。真实情况往往没那么简单。4.1 场景复现接口通了但页面还是红的有一次一个同事找我帮忙看跨域问题。他的配置看上去没问题nginx也reload了但浏览器控制台依然报Access to XMLHttpRequest at http://api.example.com/user/list from origin http://www.example.com has been blocked by CORS policy注意看这个报错里的关键信息请求的完整地址是http://api.example.com/user/listfront-end页面源是http://www.example.com。这说明前端根本还在直接请求api.example.comnginx那个代理压根没命中。为什么呢因为前端Ajax代码里写的是绝对地址配置文件中虽然做了代理但前端代码没改。代理方案有一个隐含要求前端发起请求时应该请求同源下的某个路径由nginx来转发。如果前端依然把请求地址写死成了api.example.com那请求根本不会经过nginx的代理规则浏览器拿到的还是跨域响应。解决方案也很直接把前端Ajax的baseURL统一改成相对路径/api/开头。比如原来请求http://api.example.com/user/list改为/api/user/list。页面的源是www.example.com请求路径是/api/user/list浏览器看到的是同源请求完全不触发同源策略。4.2 排查链路从Network面板到后端日志这种场景我给一套自己常用的排查链路以后遇到跨域报错就按这个顺序过一遍大概率能定位问题。第一步打开浏览器开发者工具的Network面板重新刷新页面看请求是不是发出去了。如果请求显示在列表里但状态是红色的(canceled)或者根本没有请求记录说明前端请求被浏览器拦截更早可能是页面本身的问题。第二步点击那条请求看Request URL。如果这个URL在代理规则的范围内比如以/api开头但你配置了代理继续看Response Headers里有没有Access-Control-Allow-Origin。有这个头证明响应是后端直接返回的没有经过nginx代理的路径重写没有这个头可能是响应被nginx透传了但后端自己也没加CORS头。第三步看Location和Status。不少代理配置了重定向后端返回302nginx把重定向的Location直接返回给前端前端再跳转时又把请求发到了重定向地址。如果重定向地址是一个跨域地址就又触发了CORS报错。处理办法一般是让nginx将重定向的Location改写或者让后端不要在这个场景下重定向。第四步把问题剥离。直接在浏览器里访问代理后的地址比如直接打开http://www.example.com/api/user/list看返回结果是不是预期的数据。如果这个地址能正常返回说明nginx代理没问题问题在前端代码。如果这个地址就返回404说明rewrite路径不对。第五步最后再看后端日志。跨域报错很容易让人误以为请求没有到达后端实际上很多场景下请求已经到了后端后端也正常处理了只是浏览器拒绝把响应交给前端脚本。后端日志里有没有对应请求记录能够帮你区分到底是请求没到还是响应被拦。4.3 几个容易忽略的配置细节除了路径没改、rewrite写错、Cookie domain不一致之外我再列几个实操中容易翻车的点。一是nginx配置里的location前缀匹配优先级。location /api/和location /api/user/list同时存在时精确匹配优先。如果你写了一个新的location /user/来做别的代理而请求同时能够匹配它nginx是按规则择优匹配的。我之前遇到过一个例子location /api/的代理规则没问题但后端有个接口路径里既有/api又有别的嵌套路径匹配乱了导致部分接口转发到错误的后端表现也是在浏览器端各种怪异报错。二是proxy_pass末尾的斜杠。同样的location规则proxy_pass写http://127.0.0.1:8080和http://127.0.0.1:8080/转发结果完全不同。前者会保留请求的完整uri后者会丢弃location匹配到的前缀。这个点算是最经典的坑之一。举例来说请求/api/user/listproxy_pass http://127.0.0.1:8080; 转成 http://127.0.0.1:8080/api/user/listproxy_pass http://127.0.0.1:8080/; 转成 http://127.0.0.1:8080/user/list因为proxy_pass配置差异导致后端404的案例我见过不少。三是修改配置后忘了检查语法或没重载。很多同学直接改了nginx.conf保存了然后刷新页面发现没生效就开始怀疑自己配置写错。其实nginx不会自动加载改动需要执行nginx -t确认语法然后执行nginx -s reload让配置生效。四是浏览器缓存。跨域报错调试时浏览器缓存会把你之前的失败响应缓存下来导致你改了后端CORS头后前端依然报错。开发调试时建议在Network面板里勾选Disable cache或者用CtrlShiftR强制刷新。5. 代理和CORS的边界怎么划不同场景下我选哪个方案写到这里读者应该已经有了比较清晰的认知CORS是后端配合改响应头代理方案是nginx在中间做转发给浏览器制造同源假象。但在真正的项目决策中方案选择往往不是技术最优解而是受制于资源和权限。我结合自己的经验给几个判断标准。5.1 后端接口归属权不同策略就不同如果你手里的后端接口是自己团队的或者至少你能让后端同事改代码那么优先选CORS。CORS是标准化的、被浏览器官方支持的方案配合上OPTIONS预检处理整体最干净。唯一的风险是后端逻辑复杂时可能漏掉某些响应头或某些路由。如果后端接口是第三方提供的比如你接一个小程序的登录接口、一个第三方支付接口对方没有义务配合你处理CORS那么直接用nginx代理包一层把第三方接口包装成同源接口是最省心也最可靠的做法。如果是本地开发阶段前端工程用webpack或者Vite的话我建议直接用devServer自带的proxy配置不用专门开nginx。开发态下开nginx需要改hosts、配置虚拟主机、重启服务折腾成本高。本地联调的核心诉求是快速所以用devServer或者vite proxy更合适。配置也简单以webpack为例devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } }这里要说一下changeOrigin的作用。后端在获取请求源的时候许多框架会读取请求头里的Host字段。changeOrigin设置为true表示转发时让代理服务器在请求头里把Host改写成target的域名。很多同学只写了target和pathRewrite没写changeOrigin导致后端校验来源时匹配不到也会引发奇怪的错误。到了测试和生产环境时再用nginx把这套逻辑承接过来配置几乎可以一比一平移。这就很平滑。5.2 用nginx代理时我再给几条额外的实践经验最后我把自己多年配置nginx的经验浓缩成几条建议未必全都能直接解决跨域但遇到问题时会帮你节省大量排查时间。第一个建议proxy_pass后端服务地址建议用固定的内网IP或者用域名加upstream。不要把后端地址写成公网域名否则请求绕了一圈既慢又容易碰到防火墙问题。upstream的好处是可以定义多个后端节点做负载均衡upstream backend_api { server 192.168.1.10:8080 weight3; server 192.168.1.11:8080 backup; }然后在proxy_pass里写http://backend_api。第二个建议全局统一加代理响应头减少重复配置。有时候你不需要把所有接口都改成同源路径只想针对个别接口做跨域放行那可以在location里加add_header例如location /api/ { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization; }但注意add_header在nginx里有个继承特性当前层级没有add_header时继承上一层的当前层级一旦写了add_header上一层的就被重置了。如果上游已经有别的add_header你在这个location里只加CORS头等于把上游的add_header丢了。这个细节很容易引发加了头结果别的头不见了的诡异现象。第三个建议配置安全头。如果你使用nginx不仅做跨域代理还对外直接暴露服务我建议把一些基础安全头补上。比如add_header X-Frame-Options SAMEORIGIN always; add_header X-Content-Type-Options nosniff always;always参数表示无论返回200还是4xx、5xx都会加上这些响应头。这能避免一些浏览器层面的安全问题也符合业界的基础安全规范。第四个建议日志一定要开。配置里加上access_log和error_log的路径并合理设置级别。nginx的error_log级别建议在排查阶段调到debug定位完问题后再调回notice或warn避免性能和日志量过大。error_log /var/log/nginx/error.log debug; access_log /var/log/nginx/access.log main;很多时候代理看起来没生效其实是请求被某个不匹配的location吞掉了或者rewrite正则写错了。看一眼access_log里实际打到哪台服务器、路径变成了什么问题一分钱都不用花就能定位。第五个建议如果你用了HTTPSnginx的SSL证书配置和代理是可以共存的。在server块里同时写listen 443 ssl和ssl_certificate再加location代理规则没有任何冲突。常见的问题是证书过期后浏览器提示不安全有人会误以为是跨域问题。我在一个项目里就遇到过页面证书失效浏览器把请求给拦了控制台报错看起来跟CORS很像实际上完全是两码事。我个人的体会是跨域这个问题的本质是浏览器在替用户做主决定哪些资源能碰、哪些不能碰。你没法也不应该绕过浏览器的安全机制你要做的是在更高一层的架构上通过合理的方案让来源关系清晰化。纵使跨域方案再多软件工程里那些最朴素的道理永远适用先搞清楚权限边界再决定技术选型先复现问题再动手改配置。最后分享一个小技巧调试跨域问题时不要只盯着报错信息本身把Network面板、后端日志、nginx日志这三处同时打开着问题出现时先看请求到底走到哪一步断了。这个习惯帮我省掉过很多无效沟通也让我在遇到任何奇奇怪怪的连接问题时都能快速冷静下来一步步拆解到根因。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑