
干这一行搞前后端分离几乎没人能在跨域这道坎上完全绕道走。我记得第一次带团队做项目联调前端同事跑过来指着屏幕说接口挂了我凑过去一看浏览器控制台红字写着No Access-Control-Allow-Origin header is present后端同学一脸无辜说接口用Postman测得好好的。这种场景太典型了新手上路基本都会在这里卡上一阵子。后来发现解决方案无非两条路要么后端开CORS要么用nginx做反向代理。这篇就把同源策略的来龙去脉、几种跨域方案的取舍以及nginx代理的具体配置一次讲清楚给正在跟跨域死磕的朋友一份能直接抄作业的参考。1. 同源策略到底在管什么先搞清楚游戏规则1.1 什么样的请求算“同源”什么样的算“跨域”同源策略是浏览器内置的一套安全机制它判定两个页面或请求是否属于同一个“源”。判定标准就三条协议、域名、端口三者全部一致才算同源。只要有一个对不上浏览器就认为这是跨域请求。拿实际场景举例假设前端页面跑在http://localhost:8080后端接口是http://localhost:3000。前者协议是 http域名是 localhost端口是 8080后者协议是 http域名是 localhost端口是 3000。域名一样但端口不一样于是这就算跨域。再比如https://www.example.com和http://www.example.com域名都是www.example.com但协议一个是 https 一个是 http同样跨域。www.example.com和api.example.com域名变了跨域example.com和www.example.com虽然很多人觉得是同一个站但对浏览器来说子域和主域也算不同源。这个概念挺像小区门禁门禁卡只能刷自己那栋楼的单元门换个楼就进不去哪怕两栋楼长得一模一样。域名、端口、协议三个维度就是那扇门的锁芯任何一个对不上门就不开。这里有个容易混淆的点跨域限制是浏览器行为不是服务器行为。服务器收到请求之后该处理就处理该返回就返回但浏览器收到响应后会先检查响应头里有没有放行标识如果没有就拦截下来不给页面JS使用。所以后端的日志里可能明明记着请求成功处理了前端却一直报错——这是排查跨域问题时第一个要建立的认知。1.2 同源策略限制了三件事也放行了一类请求浏览器不是什么都拦同源策略主要限制三块内容Cookie、LocalStorage、IndexedDB 等本地存储的跨源读取跨源情况下操作另一个窗口的 DOMXMLHttpRequest和fetch发起的跨源请求前两条主要防的是恶意页面偷你的登录态、篡改你在其他标签页里的页面内容。第三条是我们日常开发和接口联调时打交道最多的地方。浏览器在处理XHR/fetch跨源请求时会检查服务端返回的响应是否携带允许跨域的响应头没有就拦截。那为什么很多网页可以引用第三方CDN上的JS、加载其他域名的图片因为这些属于“标签请求”script src、img、link这些标签天然不受同源策略限制。最早期的跨域方案JSONP就是钻这个空子实现的后面会细说。同源策略存在的意义往大了说是为了防 CSRF跨站请求伪造和 XSS跨站脚本攻击。如果没有这层限制你登录了某个网站再去逛一个恶意站点恶意站点的脚本就能顺势向那个网站发起带Cookie的请求借你的身份做转账之类的操作。所以浏览器宁可把规则定死一点也不给恶意脚本留可乘之机。2. 跨域方案怎么选CORS、JSONP、代理一次讲透2.1 CORS服务端开门浏览器才放行CORS跨源资源共享是目前最正统、最主流的跨域方案。它的思路很简单浏览器不会凭空白放行跨域请求除非服务端在响应头里明确声明“这个源可以访问我”。后端在做Java、PHP、Node开发时只要在接口响应里加几个响应头Access-Control-Allow-Origin允许哪些源访问可以指定具体源也可以配*表示所有源Access-Control-Allow-Methods允许哪些HTTP方法常见的有GET、POST、PUT、DELETE、OPTIONSAccess-Control-Allow-Headers允许请求携带哪些自定义头比如Content-Type、AuthorizationAccess-Control-Allow-Credentials是否允许携带Cookie。注意一点它设为true的时候Allow-Origin不能是*必须写成具体的源Access-Control-Max-Age预检请求结果的缓存时间单位秒。配了之后浏览器在一段时间内不需要重复发送OPTIONS请求CORS的请求分两种。一种是简单请求比如用 GET 或 POST且Content-Type是application/x-www-form-urlencoded、multipart/form-data、text/plain这类发起的请求浏览器直接带上实际请求发过去。另一种是复杂请求比如自定义了Authorization请求头、或者Content-Type用了application/json浏览器会先发一个OPTIONS预检请求服务端确认允许后才发真正的业务请求。实际踩过的坑是后端只给业务接口加了CORS响应头却忘了处理OPTIONS预检请求导致前端在网络面板里看到一片OPTIONS请求返回404或非2xx状态码业务请求也发不出去了。所以写后端跨域配置时最好对整个路径范围统一加响应头别只盯着业务接口。各语言配置CORS其实都不难。后端用 PHP 的话就是在入口处加header(Access-Control-Allow-Origin: *);。Java SpringBoot 可以有注解CrossOrigin或者写一个全局的WebMvcConfigurer配置类。Node 里用 Express 的话加个cors中间件就行。具体看团队后端用的什么框架关键是理解上面那几个响应头的含义别硬背代码。2.2 JSONP上古方案还能撑住GET接口JSONP 属于钻空子方案利用的是script标签不受同源策略限制这个特性。原理是前端动态创建一个script标签它的src指向跨域接口地址并带上一个回调函数名参数。后端收到请求后把数据包在一个 JavaScript 函数调用里返回。前端拿到响应后这个“脚本”会直接执行从而把数据传给回调函数。比如前端请求http://api.example.com/getUser?callbackhandleUser后端返回handleUser({ name: 张三 })。浏览器把它当脚本执行handleUser就被调用了。JSONP 的局限非常明显只能支持 GET 请求没法拿到 POST、PUT、DELETE而且script标签加载失败时很难精确捕获错误状态排障体验很差。还有个安全隐患如果接口被恶意站点拿去用回调函数名又没做校验容易被人利用。所以现在新项目里基本不推荐 JSONP最多在维护老系统、对接别人已经写死的接口时用一用。2.3 iframepostMessage跨窗口通信的另类思路如果两个页面挂在不同域名下又想互相传数据可以用postMessage方案。它允许不同源的窗口之间安全地发送消息用的时候在主窗口监听message事件在 iframe 里调用parent.postMessage(data, targetOrigin)发送数据。这套方案的适用场景是页面嵌入了第三方 iframe需要双方通信比如嵌入登录框、支付组件。它解决的不是普通的接口跨域问题而是“跨窗口通信”问题。如果做的是标准的前后端分离前后端之间走HTTP接口优先考虑的还是CORS或者nginx代理。2.4 方案选型对比什么场景用什么写个简单的选型参考直接做决策用方案适用场景优点缺点CORS前后端都能改接口对外提供服务的场景标准方案、支持各种请求方法、浏览器兼容好后端需要改代码跨域配置太多会显得繁琐JSONP只读接口、老系统维护、后端无法改响应头的场景实现简单、兼容IE老版本仅支持GET错误不好捕获有安全隐患nginx反向代理前后端分离联调、生产环境统一入口、后端不想/不能改CORS前端后端都不用改代码浏览器角度看完全同源需要一台nginx服务器或本地环境配置文件要维护postMessageiframe嵌套跨域页面需要通信能实现跨窗口双向通信事件监听容易混淆不适合替代HTTP接口调用从我个人的实际情况看CORS是每个后端开发都得会的基础能力nginx代理则是前端开发、运维、全栈在联调和部署阶段的必备武器。很多团队“开发环境用代理、生产环境用CORS”的搭配就是结合这两条路的优势来的。3. nginx反向代理实战一套配置解决前后端分离跨域3.1 nginx安装与基本操作在聊代理配置之前先把环境准备好。nginx的安装方式挺多不同操作系统不一样。在LinuxUbuntu/Debian上可以执行sudo apt install nginx直接装CentOS/RHEL系用sudo yum install nginx。装完用nginx -v验证版本再用systemctl start nginx启动服务。在Windows开发机上去nginx官网下载Windows版的压缩包解压到一个不带中文的路径比如D:\nginx-1.24.0直接双击nginx.exe就能跑起来。Windows下没有service脚本所以停止服务得用nginx.exe -s stop重载配置用nginx.exe -s reload。如果本机装了Docker一条命令也能拉起nginxdocker run -d -p 80:80 --name nginx -v /path/to/nginx.conf:/etc/nginx/nginx.conf nginx个人习惯是在开发机上直接用原生nginx因为改配置后 reload 特别快也不会出现端口映射造成的认知偏差。生产环境的话我偏好用Docker部署配置文件、证书、静态资源都挂在目录里迁移和回滚都方便。无论哪种方式修改配置文件之前养成一个习惯先跑nginx -t检查语法。这个命令会告诉你配置文件有没有写错错在哪里。改完配置之后记得nginx -s reload重新加载而不是重启reload是平滑重载不会中断已有连接。3.2 反向代理为什么能绕开同源策略先想清楚一个关键问题同源策略是浏览器检查页面源和请求目标源是否一致。nginx反向代理做的事情是让浏览器发出的所有请求都打到nginx上再由nginx转发给后端服务器。浏览器只感知到“我访问的和页面所在的是同一个源”因为请求的URL、端口跟页面完全一致。后端接口返回的数据经过nginx原样带回给浏览器浏览器不会拦。整个过程中浏览器没有直接向后端发请求自然也就不存在跨域问题。这就好比你在公司内部有个需要刷卡才能进的保密室正常情况下外人进不去但你雇了个前台小哥nginx访客只需要到前台报一声需求小哥进去把资料拿出来给访客。访客全程没接触保密室的门禁所以压根不需要刷卡资格。对开发来说这套方案的最大价值在于前端不用改任何代码后端也不用配CORS。特别是项目里后端接口动不动有几十个团队之间又要频繁联调配nginx代理是效率最高的方式。3.3 前后端分离下的完整配置示例假设开发环境的实际布局是前端项目跑在http://localhost:8080webpack dev server后端接口跑在http://localhost:3000。期望的效果是浏览器通过http://localhost/api/user访问后端接口。用nginx在80端口做统一入口。配置如下server { listen 80; server_name localhost; # 前端静态资源目录比如构建产物 root /data/www/frontend; index index.html; # 前端页面路由 location / { try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有几个点值得展开说。第一root指向前端静态文件目录放的是构建产物比如Vue项目的dist目录、React项目的build目录。浏览器访问http://localhost/时nginx直接把这个目录下的index.html返回给前端。第二try_files $uri $uri/ /index.html;主要解决前端路由的 history 模式刷新404问题。单页应用里的路由切换靠的是前端JS但用户手动刷新/user/list这个路径时nginx会先找对应的真实文件找不到就回退到index.html让前端路由接管。少了这一行刷新二级页面容易出现404。第三location /api/里配置proxy_pass http://127.0.0.1:3000;代表所有以/api/开头的请求都被转发到http://127.0.0.1:3000。后端接口如果有/api前缀这一条就完全够用如果后端接口本来没有/api前缀比如实际路径是/user转发时想去掉/api部分proxy_pass后面就要加一个斜杠写成http://127.0.0.1:3000/;。proxy_pass带不带末尾斜杠这是个特别经典的坑。举例说明请求进来是/api/user如果proxy_pass http://127.0.0.1:3000;不带斜杠转发给后端的是/api/user如果proxy_pass http://127.0.0.1:3000/;带斜杠转发给后端的是/user。按后端实际的路由来选别凭感觉写。第四proxy_set_header这三行是标配。Host $host把请求的Host头传给后端X-Real-IP和X-Forwarded-For传递真实客户端IP。后端如果要记录用户IP、做限流这些都依赖代理把这些头带过去否则拿到的一律是nginx的地址。这个配置改完后前端开发时请求地址直接写http://localhost/api/xxx就不会再被跨域拦。前端项目的 devServer 或者代码里配置的 baseURL 也要相应改到http://localhost/api下保持统一。3.4 多后端服务、WebSocket等进阶场景实际项目里前端页面同时调好几个后端服务也是常事。一个nginx脚本可以轻松挂多个location块每个块代理到不同的后端端口server { listen 80; server_name localhost; location /api/user/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; } location /api/order/ { proxy_pass http://127.0.0.1:4000; proxy_set_header Host $host; } }这样前端可以统一请求http://localhost/api/user/login和http://localhost/api/order/list浏览器视角全是同源请求后端各自处理各自的前缀互不干扰。如果团队里还有人负责Java、有人负责Go这个方法可以省掉一堆CORS配置沟通。再一个常见场景是WebSocket代理。前端用ws://localhost/ws建立长连接如果后端单独起了一个WebSocket服务需要单独配一个location并显式设置升级请求头location /ws/ { proxy_pass http://127.0.0.1:3001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 60s; }不配置Upgrade和Connection这两个头的话WebSocket握手会直接失败表现为连接一直在pending状态然后断开。proxy_read_timeout 60s是长连接空闲超时设置如果业务里聊天消息不频繁建议调大一些避免连接被nginx主动断开。在生产部署里nginx还能顺带托管前端静态资源、做HTTPS证书配置、对静态文件启用缓存。比如给图片、JS、CSS加缓存头location ~* \.(js|css|png|jpg|gif|svg|webp)$ { expires 7d; add_header Cache-Control public, max-age604800; }这样静态资源由nginx直接返回后端只需要专心处理接口前端和后端彻底收敛到同一个源下跨域问题直接消失。4. 跨域排查实录那些让你怀疑人生的报错4.1 浏览器缓存伪装成跨域的老熟人有一种很坑的情况后端明明配置好了CORS网上搜到的教程也都试了但浏览器照样报跨域错误。这时候先别急着怀疑配置大概率是浏览器缓存捣的鬼。Chrome会对OPTIONS预检请求的结果做缓存缓存时间由Access-Control-Max-Age控制。如果开发过程中你调过一次接口当时后端还没配好CORS浏览器可能已经缓存了那次“不允许访问”的响应后面后端把配置补上了但浏览器短期内存里还记着旧结果继续拦截。处理办法也简单第一在浏览器里强刷一次页面快捷键是CtrlShiftR或者直接开一个无痕窗口第二后端给OPTIONS响应头加上Cache-Control: no-cache第三调低Access-Control-Max-Age开发环境建议干脆不配避免调试时一直吃缓存。还有个很容易看走眼的点浏览器的开发者工具Network面板里如果请求显示为(cancelled)很多情况下不是跨域而是前端页面自己取消了请求比如组件卸载、路由切换。别一看到cancelled就当成跨域排查。4.2 localhost和127.0.0.1的“身份”问题浏览器认为http://localhost:8080和http://127.0.0.1:8080是不同源虽然它们指向的都是本机。开发的时候前端如果一边用http://localhost:8080打开页面一边又在CORS配置里写了http://127.0.0.1:8080那请求照样是跨域而且报错信息里的Origin字段会明确写着http://localhost:8080跟后端配置里对不上。这个问题的排查成本很低但坑过不少人。解决方案是开发时统一访问地址要么全用localhost要么全用127.0.0.1。前后端联调时最好把这个约定写进团队文档里否则每家本地环境用的地址不一样联调效率会很难看。4.3 HTTPS页面访问HTTP接口的双重拦截页面部署在HTTPS环境下接口地址却是http://开头这在浏览器里会触发“混合内容”拦截。Chrome会把这类请求直接block掉报错信息也不一定直接写“跨域”有时候是Mixed Content: The page at https://... was loaded over HTTPS, but requested an insecure resource。这种问题的本质是跨域混合内容的组合。解决办法就是让前端页面和接口统一走HTTPS或者用nginx反代把HTTPS请求转发给HTTP后端。配HTTPS时nginx里要加ssl_certificate和ssl_certificate_key配置证书怎么申请这里不展开但思路是明确的页面和接口的源尽量保持一致让前端的请求在同源下结束。4.4 nginx配置不生效的排查路径如果走nginx代理方案却发现没生效按下面的顺序排查基本能把问题定位到具体环节先跑nginx -t确认配置语法没问题。查看logs/error.logLinux下一般是/var/log/nginx/error.log看看有没有路由匹配相关的报错。确认配置修改后执行过nginx -s reload这个命令是平滑重载不会断连接。检查80端口有没有被占用。Windows下netstat -ano | findstr :80Linux下ss -lntp看看端口是不是被别的服务抢占了。用curl直接验证转发是否正常curl -v http://localhost/api/user看返回的到底是什么内容。如果curl拿到的响应是对的说明nginx配置没问题问题在浏览器侧。这里有一条独家经验配置nginx代理时要先用curl打通链路再去开浏览器调试。curl不带浏览器的那套同源限制如果curl都拿不到后端数据说明是域名解析、端口、location匹配的问题跟跨域无关如果curl正常、只有浏览器报错那问题一定出在浏览器端继续查缓存和Origin即可。常见问题做个小表直接对着查症状可能原因处理方向控制台报 No Access-Control-Allow-Origin后端未配置CORS响应头后端配置响应头或用nginx代理请求出现在Network面板但状态是failed浏览器拦截了预检请求检查OPTIONS请求是否被正确处理修改nginx配置后没效果忘了reload或配置语法错误执行nginx -t再nginx -s reload刷新页面后路由404前端history路由没配try_files在location / 里加try_files回退到index.html页面是HTTPS接口是HTTP混合内容被浏览器直接拦截接口反代成HTTPS统一源localhost和127.0.0.1本机却跨域浏览器认为子域和主域、不同host为不同源统一用同一个host访问这些坑我一个一个都踩过。尤其是proxy_pass的斜杠问题和浏览器OPTIONS缓存几乎每次教别人做跨域都要提一遍。我个人实际用的套路组合是开发联调阶段一律用nginx代理前端代码里的baseURL指向本地nginx地址后端不用动任何代码生产环境先看后端能不能配CORS能配就配不能配就也用一个nginx入口把前端静态资源和后端接口挂到同一个域名下。nginx配置文件和注意事项一定放进项目仓库的docs目录里新同事拉下来照着跑半小时内本地环境就通了不用四处打听“跨域怎么解决”。最后再分享一个排查小技巧Chrome的Network面板里跨域报错会挂在Console里但具体的请求细节要从Headers页面看Origin和Access-Control-Allow-Origin两个字段对不对应。浏览器控制台的报错信息其实已经写清楚了是谁放的拦路牌大多数时候是后端响应头缺失看一眼响应头就知道该找谁改配置省得前后端互相甩锅。