ARTICLE DETAIL

资讯详情

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

前端跨域终极指南:同源策略、CORS与nginx反向代理

前端跨域终极指南:同源策略、CORS与nginx反向代理 做前端开发的十有八九都见过这行报错“No Access-Control-Allow-Origin header is present on the requested resource”。刚入行的同事往往会满头问号——接口用 Postman 测得好好的代码看起来也没问题怎么一放到浏览器里就报跨域这不是代码写错了而是浏览器的同源策略在“执行公务”。同源策略是浏览器最基础也是最重要的安全机制之一它决定了页面只能访问和自己“出身”相同的资源。可如今前后端分离架构几乎是标配前端跑在 localhost:8080接口部署在 api.example.com同源策略就成了开发路上绕不开的一道坎。解决跨域的办法不少CORS、JSONP、nginx 反向代理各有适用场景而其中 nginx 反向代理因为配置简单、性能稳定、还能顺带解决开发和生产的部署问题是很多团队的最终选择。这篇文章会把同源策略的来龙去脉讲清楚再把 CORS、JSONP、nginx 代理这些方案从头到尾演示一遍适合正在被跨域问题折磨的前端、后端和运维同学参考。1. 同源策略浏览器到底在拦什么1.1 同源的定义协议、域名、端口三件套“源”origin由三部分组成协议、域名、端口。只有三者完全一致才叫同源。举个例子页面地址是https://www.example.com那么https://www.example.com/api是同源因为域名和协议端口都一样http://www.example.com协议不同是跨域https://api.example.com域名不同是跨域https://www.example.com:8080端口不同也是跨域。这里有个容易忽略的细节默认端口。https 默认走 443http 默认走 80所以https://www.example.com:443和https://www.example.com会被浏览器视为同一个源。但你要是写成https://www.example.com:8443那就是另一个源了。还有localhost和127.0.0.1看起来是同一个东西浏览器却严格按字符串比较认为它们是两个不同的源。这个坑我踩过不止一次开发环境里前端页面用 localhost 打开接口地址却写 127.0.0.1结果死活报跨域查了半天才发现居然是这个原因。所以排查跨域问题第一步永远是先把“源”的三要素在纸上列清楚。1.2 为什么要设这道坎从 CSRF 说起同源策略不是故意给开发者添堵它保护的是用户的数据安全。设想一个场景你在银行网站 A 的账户页面登录了浏览器里保留着 A 的登录态 Cookie。这时你打开了另一个恶意网站 BB 页面里放了一张图片或一个表单请求指向 A 的“转账”接口。如果没有同源策略B 发出的请求会带着 A 的 Cookie 一起送到银行服务器银行接口收到请求后以为是用户本人操作钱就被悄悄转走了。这就是经典的 CSRF跨站请求伪造攻击。同源策略把这个漏洞堵上了B 页面的脚本只能读取 B 自己的数据发往 A 的请求会被浏览器拦截住A 的接口根本收不到带用户身份信息的请求。理解了这一点你就明白了——我们做跨域访问不是在“绕过”同源策略而是要用浏览器能认可的安全方式让非同源资源之间合法通信。这也是为什么 CORS 要由服务器端显式声明允许来源为什么 nginx 反向代理能成为主流方案因为这两种方式都没有破坏安全模型只是让“源”的判断结果发生了变化。1.3 同源策略的边界哪些被拦、哪些不拦同源策略管的其实不是所有网络请求它主要拦的是“跨域请求的响应读取”。换句话说发起跨域请求本身并不全被禁止比如img标签加载跨域图片、script标签加载跨域 JS、link标签加载跨域 CSS这些都是允许的。真正受限的是通过fetch或XMLHttpRequest发起的跨域请求以及页面脚本对这些请求响应的读取。还有一个非常关键的事实跨域请求发出去了服务器也正常处理了数据也返回了但浏览器在最后一步把响应扣下了控制台才报错。很多后端同学说“我接口明明通了数据库都查到了数据前端还报跨域”原因就在这里——请求到了响应也回了只是浏览器基于安全策略不让页面脚本拿到。这个认知很重要否则你会反复在“接口可用性”上做无用功而真正的瓶颈在浏览器这一层的校验规则。2. 跨域问题的常见解法与选型思路2.1 方案一后端开启 CORS最正统的解法CORSCross-Origin Resource Sharing跨域资源共享是 W3C 的标准方案核心思路是“由服务器声明允许哪些来源访问”。后端在响应头里加几个关键字段就能实现Access-Control-Allow-Origin允许的来源可以是具体域名https://www.example.com也可以是*通配所有来源但*不能和允许凭证功能同时使用。Access-Control-Allow-Methods允许的 HTTP 方法常见写法是GET, POST, PUT, DELETE, OPTIONS。Access-Control-Allow-Headers允许的自定义请求头比如Content-Type, Authorization。如果需要携带 Cookie还要加Access-Control-Allow-Credentials: true。CORS 分“简单请求”和“预检请求”两种情况。满足以下条件的叫简单请求方法只能是 GET/POST/HEAD请求头只用浏览器自动加的那些Content-Type只能是application/x-www-form-urlencoded、multipart/form-data、text/plain之一。简单请求直接发出不需要提前打招呼。但只要你用了application/json的 Content-Type或者带了Authorization这类自定义头浏览器就会先发一个 OPTIONS 预检请求问服务器“我这样跨域行不行”服务器回应了正确的 CORS 头浏览器才发真正的业务请求。这里就是后端同事一头雾水的地方日志里看到一堆 OPTIONS 请求以为有人在扫描其实那是浏览器在“探路”。后端接口如果只处理了 GET 和 POSTOPTIONS 直接返回 404 或空响应预检就失败了前端看到的依然是跨域报错。标准做法是后端对 OPTIONS 请求统一返回 204并带上允许的 Headers 和 Methods。2.2 方案二JSONP老古董但某些场景还能用JSONP 的思路非常“野路子”既然script标签加载跨域 JS 不受限制那就让服务器返回一段 JS 代码把数据包在一个回调函数里前端提前定义好这个函数。比如前端写script function handleData(data) { console.log(data); } /script script srchttps://api.example.com/user?callbackhandleData/script服务器返回的内容是一段handleData({ name: 张三 })浏览器把它当作 JS 执行数据就到了前端。这个方案的好处是不需要后端配置任何 CORS 头缺点是只能支持 GET 请求没法用 POST、PUT也不能带自定义请求头错误处理还很别扭接口超时或报错时根本没有统一的状态码告诉你。现在主流后端框架基本都支持 CORS 了JSONP 的使用场景越来越窄主要是些老系统、或者第三方公开接口只提供 JSONP 时才会用到。我的建议是新项目不要主动选 JSONP除非你面对的是一个完全没有 CORS 支持且只能 GET 的存量接口。2.3 方案三代理转发开发和生产的通用路子第三种解决思路最关键既然浏览器是“看来源”的那让页面请求的地址和页面自己完全同源问题不就消失了具体做法是前端页面部署在 A 域名后端接口在 B 域名中间加一层代理前端请求统一写 A 域名下的某个路径比如/api代理服务把/api开头的请求原样转发给 B 域名的真实接口再把响应原样返回。浏览器看到的是页面地址是https://www.example.com请求地址也是https://www.example.com/api完全同源同源策略根本不会介入。而真正的接口调用发生在代理服务器和后端服务器之间服务器之间没有同源策略的限制想怎么请求就怎么请求。这个方案在前后端分离项目里使用率非常高开发环境用 Node 的 devServer 代理生产环境用 nginx 反向代理一套思路贯穿始终切换成本极低。2.4 方案对比什么时候选哪个方案改动成本适用场景局限性CORS 响应头后端加配置成本低接口要开放给多个域名调用预检请求处理不当容易踩坑JSONP前端后端都要改老接口、只读数据仅 GET错误处理弱nginx 反向代理运维配置一次成本低前后端都是一方部署需要可控的服务器环境postMessage前端代码改动iframe 跨窗口通信不适用于普通接口请求WebSocket前后端各自支持实时推送、长连接需要额外处理握手升级选型逻辑其实很清晰如果接口将来要面向第三方开放CORS 是必须的如果只是自己团队的前后端项目nginx 反向代理通常是最稳健的。二者不是互斥的很多生产系统会并存——对外提供 API 的网关用 CORS对内前后端同域部署用 nginx 转发。3. nginx 反向代理原理与配置详解3.1 反向代理为什么能解决跨域nginx 的配置本身不难难的是理解它在整个链路里的位置。所谓“反向代理”就是 nginx 作为中间人接收浏览器的请求按照配置规则把请求转发给后面的真实服务器再把真实服务器的响应送回给浏览器。对浏览器来说它只知道自己正在和 nginx 通信根本不知道 nginx 后面还有别的服务器。这正是我们想要的“障眼法”浏览器以为一切都是同源的。生产环境最常见的架构是一个 nginx 监听 80/443 端口location /指向前端静态文件目录比如 Vue 或 React 打包后的 distlocation /api用proxy_pass指向后端接口服务比如http://127.0.0.1:9000。浏览器访问https://www.example.com拿到前端页面页面里请求https://www.example.com/api/usersnginx 把请求转发到http://127.0.0.1:9000/api/users后端返回数据nginx 原样吐给浏览器。全程同源零跨域报错。3.2 基础配置静态文件加接口转发一个最小可用的配置长这样以 Ubuntu 系统、配置文件位于/etc/nginx/sites-available/为例server { listen 80; server_name www.example.com; # 前端静态资源 location / { root /var/www/dist; index index.html; try_files $uri $uri/ /index.html; # 前端 history 路由必需 } # 接口反向代理 location /api/ { proxy_pass http://127.0.0.1:9000/; } }其中try_files $uri $uri/ /index.html是给单页应用用的。前端路由如果采用 history 模式用户直接访问https://www.example.com/user/123时磁盘上没有这个文件nginx 需要把所有请求回退到index.html由前端路由自己解析。少了这一行刷新页面就是 404。很多同学配完 nginx 发现“首页能开、一刷新就白屏”原因就在这。3.3 最容易踩的坑proxy_pass 的斜杠问题proxy_pass最后有没有斜杠转发结果完全不一样这是新手翻车率最高的地方。规则可以总结成几点location /api/加proxy_pass http://127.0.0.1:9000/请求/api/users时location 匹配到的/api/会被替换成/后端收到的路径是/users也就是说/api前缀被吃掉了。location /api/加proxy_pass http://127.0.0.1:9000末尾没有斜杠请求/api/users原样转发后端收到的还是/api/users。location /api不带尾斜杠加proxy_pass http://127.0.0.1:9000/users此时/api会被替换成/users请求/api/list转发后变成/users/list。我建议你改写配置时在纸上画一遍路径拼接或者直接用curl验证到底转成了什么。实际项目中最常见的判断依据是后端接口本身带不带/api前缀。如果 Spring Boot 这类后端用/api作为统一路由前缀nginx 就写location /api/ { proxy_pass http://127.0.0.1:9000; }保持原路径如果后端接口是干净的/user、/ordernginx 就需要写带尾斜杠的proxy_pass http://127.0.0.1:9000/;把/api剥掉后再转发。3.4 缺一不可的请求头与超时配置前面那三行配置在开发环境够用但一上线就会遇到各种“玄学”问题排查到最后基本都是请求头和时间超时没配。推荐在location /api/里补上这一组location /api/ { proxy_pass http://127.0.0.1:9000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 10s; proxy_send_timeout 60s; proxy_read_timeout 60s; }为啥要设置Host头因为后端服务器有时会根据 Host 做路由或域名校验不设置的话nginx 默认把后端的地址当作 Host 传过去后端可能直接拒绝。设置成$host后端看到的就是浏览器访问的原始域名。X-Real-IP和X-Forwarded-For则是把真实客户端 IP 透传给后端否则后端的访问日志里清一色都是 nginx 服务器的 IP做统计、做限流、按 IP 封禁的规则全部失效。超时配置也很有讲究。proxy_connect_timeout控制的是 nginx 和后端建立连接的超时时间后端没起来或者网络不通时这里会先报错proxy_read_timeout控制的是 nginx 等待后端响应的时间遇到查询慢的接口比如导出报表花费一两分钟不把这段调大前端就会收到 504。这三行看起来不起眼排查线上问题的时候能救命。3.5 带 Cookie 的请求和 WebSocket 的特殊配置如果前端请求带了登录凭证 Cookienginx 默认就会透传 Cookie 头一般不需要额外处理但有一个前提前端用fetch或 axios 时要把credentials设置为include或same-origin。如果走的是 nginx 同源代理浏览器看到的是同源请求不触发 CORS 校验Cookie 默认携带这块反而省心。真正要注意的是后端返回的Set-Cookie如果带着Secure属性在纯 HTTP 环境下浏览器不会写入需要保证全链路 HTTPS。WebSocket 的代理配置是另一个高频盲区。想通过 nginx 转发ws://升级请求需要这样配location /ws/ { proxy_pass http://127.0.0.1:9500/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }关键在第 3、4 行WebSocket 握手是 HTTP 协议带着Upgrade: websocket头发起的nginx 默认走 HTTP/1.0 连接不支持这种协议升级必须把proxy_http_version设为 1.1并把 Upgrade 头透传过去。proxy_read_timeout也要调大因为 WebSocket 是长连接默认 60 秒没有任何数据交互就会被掐断实时的聊天、行情推送功能就会时不时掉线。4. 实操复盘一个前后端分离项目的跨域改造4.1 场景描述与整体思路我用一个典型项目来演示完整流程前端是 Vue 3 项目开发时跑在http://localhost:8080后端是 Spring Boot跑在http://localhost:9000接口路径是/api/login、/api/user/list这种带/api前缀的形式。生产环境计划把前端构建产物 dist 部署到服务器上用 nginx 同时托管静态文件和转发后端接口最终统一走https://www.example.com。整体思路分三段开发环境用 vite 的 devServer 代理解决跨域生产环境用 nginx 反向代理解决跨域两处原理完全一致都是“前端请求同源路径、代理转发到真实后端”。先跑通开发环境再迁移到 nginx每一步都有明确的验证方法出问题知道查哪里。4.2 开发环境vite 代理配置Vue 项目使用 vite 的话在vite.config.js里加一段export default defineConfig({ server: { port: 8080, proxy: { /api: { target: http://localhost:9000, changeOrigin: true // 后端接口本身不带 /api 前缀时打开下一行 // rewrite: path path.replace(/^\/api/, ) } } } })前端代码里所有请求统一写/api/xxxvite 启动的 Node 服务接手请求后转发给http://localhost:9000。changeOrigin: true会把请求头里的 Host 改成目标地址的 Host避免后端做域名校验时报错。浏览器看到的请求地址始终是http://localhost:8080/api/xxx同源不报跨域。rewrite注释那行和 nginx 斜杠问题的逻辑一样后端带/api前缀就保留原路径不带就去掉前缀按实际情况选择。4.3 生产环境nginx 完整配置生产环境我贴一份可以直接“抄作业”的完整配置记得把域名、路径替换成自己的upstream backend_server { server 127.0.0.1:9000; keepalive 32; } server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/ssl/example.com.pem; ssl_certificate_key /etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; gzip on; gzip_types text/plain text/css application/javascript application/json; location / { root /var/www/example/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Connection ; proxy_connect_timeout 10s; proxy_read_timeout 60s; } }这里用upstream定义后端服务器组后续后端要扩容成多台机器只需在 upstream 里多加几行server IP:PORT;nginx 自动做负载均衡前端和配置主体完全不用动。keepalive 32让 nginx 和后端之间保持长连接避免每次请求都重新建连高并发场景下收益明显。gzip 压缩对前端静态资源的体积改善非常直观尤其是未压缩的 JS 和 CSS 能缩小到原来的四分之一。配置写完后先执行nginx -t检查语法再systemctl reload nginx平滑重载。如果改了 listen 端口或证书这类关键配置reload 可能不奏效需要systemctl restart nginx。用nginx -t先验证是铁律见过不止一次同事直接 restart语法写错导致服务起不来线上秒级告警场面一度很紧张。4.4 验证方法从 curl 到浏览器开发者工具配置完别急着宣布“搞定”我有一套固定的验证顺序。第一步用 curl 直接测 nginx 转发链路curl -i http://localhost/api/user/list返回了后端数据说明 nginx 到后端的链路是通的。注意这一步是在服务器本机执行浏览器没有参与所以不会触发跨域校验看不到跨域报错是正常的别误判。第二步打开浏览器访问前端页面按 F12 打开开发者工具切到 Network 面板找到/api/xxx那个请求看响应头和响应体。同源代理场景下响应头里通常不需要Access-Control-Allow-Origin因为浏览器根本不会做跨域校验。如果后端自己加了 CORS 配置也不冲突但要留意别出现重复配置导致“两边都在管”的混乱。第三步清掉浏览器缓存再测一次。跨域报错有时候是真的有时候是浏览器缓存了旧响应尤其是有 Service Worker 的项目缓存能把问题掩盖很久。实测中“改了配置没生效最后发现是浏览器缓存”的比例高得惊人尤其是本机同时开着多个前端服务的情况。5. 常见问题与排查技巧实录5.1 浏览器报错汇总速查表把这些年遇到的高频跨域类报错整理成一张速查表方便照着定位报错现象可能原因优先排查方向No Access-Control-Allow-Origin header is present后端没配 CORS或代理未到后端用 curl 直接请求后端接口看响应头Response to preflight request doesnt pass access control checkOPTIONS 预检没被正确响应检查后端对 OPTIONS 的处理确认 Allow-Headers 覆盖了实际请求头The request client is not a secure context页面是 HTTP但浏览器要求安全上下文全链路升级 HTTPSNetwork Error部分浏览器不给细节代理目标不可达、端口不通、防火墙拦截查 nginx error.logping 和 telnet 目标端口504 Gateway Timeout后端处理超时调大 proxy_read_timeout查后端慢接口页面能开接口 404proxy_pass 斜杠问题或路径前缀不匹配核对 location 与 proxy_pass 的斜杠组合刷新前端路由 404缺少 try_files 回退配置补上try_files $uri $uri/ /index.html;这张表在团队内部一直贴着遇到跨域问题先对着查一遍大部分情况能当场定位。5.2 案例一预检请求 404排查了半小时有一次同事找我前端 POST 一个 JSON 格式请求控制台报跨域后端日志里却只有一条 OPTIONS 请求 404没有任何 POST 记录。这就是典型的预检失败。他们的后端用的老框架路由里根本没有 OPTIONS 方法对应的处理器预检请求直接被 404 弹回去。解决办法是在后端加一个对 OPTIONS 请求的统一处理只要方法是 OPTIONS直接返回 204并带上下面这些头Access-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With注意Access-Control-Allow-Headers里的值必须覆盖前端实际发送的每一个自定义头少一个预检就失败。浏览器报错信息其实已经把答案说得很清楚——“Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response”可惜很多人没耐心读完这条英文就到处查网络配置了。5.3 案例二Cookie 带不上后端不认登录态另一个高频问题是前端登录成功之后后续请求总是 401。排查下来发现是 axios 默认不携带 Cookie需要在请求配置里打开withCredentials: true。如果用的是 fetch要写成fetch(/api/user/info, { credentials: include })还有一个隐藏条件如果走的是直连后端接口的 CORS 方案服务器返回的Access-Control-Allow-Origin不能是*必须写具体域名同时Access-Control-Allow-Credentials要为true。而走 nginx 同源代理时基本不用为这些发愁因为同源请求不涉及 CORS 校验Cookie 默认就会带上。如果走了代理 Cookie 还是丢重点检查 Cookie 的SameSite属性是否设得太严格以及前端代码是否在请求拦截器里手动删掉了 Cookie 头。5.4 案例三本地没问题部署服务器就 502本地开发一切正常部署到服务器后接口全挂 502这种问题十有八九出在环境差异。第一反应是查 nginx 的 error.log路径通常在/var/log/nginx/error.log。常见原因有三类第一后端服务没起来或者端口不对。在服务器上直接执行curl http://127.0.0.1:9000/api/user/list连不通就回后端看systemctl status和后端日志。第二防火墙或云安全组没放行。关键是即使 nginx 和后端在同一台服务器通常不太会撞防火墙但后端在另一台机器时nginx 所在机器必须能访问目标机器的 9000 端口安全组和 iptables 都得查。第三SELinux 捣乱。这是 CentOS 系服务器的专属坑SELinux 默认策略可能拦截了 nginx 发起网络连接的权限。临时验证可以执行setenforce 0如果立刻恢复就是 SELinux 的问题。长期解决是给 nginx 打开对应布尔值比如setsebool -P httpd_can_network_connect 1。5.5 实用排查思路链路三分法最后分享一个我一直在用的跨域排查思路叫“链路三分法”——把完整链路拆成“浏览器到 nginx”“nginx 到后端”“后端到数据库”三段每段单独验证浏览器到 nginx看 Network 面板里请求是否发出、返回什么状态码、响应头有没有异常。nginx 到后端在服务器上 curl 代理地址和 curl 后端直连地址的返回做对比不一致说明 nginx 转发环节有问题。后端到数据库看后端日志确认 SQL 是否执行、是否报错。只要某一段是通的就说明那一段没问题逐段排除之后问题必然集中在“不通的那一段”。这套思路不需要多高深的技巧但比对着报错瞎猜高效得多特别适合新手建立排查的信心。跨域问题的本质永远是“源”的判断和服务器的响应把这两件事想清楚大部分坑都能提前避开。结合这几年的实际操作我个人的体会是不要一上来就想着绕过同源策略也不要盲目选方案。CORS 适合接口要开放给多个外部域名调用的场景nginx 反向代理适合前后端都是自己团队的 Web 项目因为它在解决跨域的同时还顺手把静态资源托管、HTTPS、负载均衡这些事全办了一套配置打通整个生命周期。如果给刚入行的前后端同事一个建议那就是先把“源”的概念彻底搞明白再用 curl 把 nginx 转发链路验证一遍剩下的都是水到渠成的事。最后分享一个小技巧nginx 配置改完之后习惯性地开着tail -f /var/log/nginx/access.log你在浏览器里的每一次点击请求日志里都会出现对应记录。它能让请求“到底到没到 nginx”一目了然排查效率翻倍。
返回列表