Nginx解决前端跨域:同源策略与反向代理配置实战

发布时间:2026/10/7 4:12:51
Nginx解决前端跨域:同源策略与反向代理配置实战 前后端分离做久了跨域这问题就像夏天的蚊子不咬人但烦人。尤其项目一上线浏览器控制台那一排红字一看又是Access-Control-Allow-Origin前端和后端互相甩锅的戏码天天上演。我自己的习惯是能用Nginx解决就不去改业务代码毕竟换了个环境跑起来还得重新调太浪费时间。这篇文章就把我用Nginx处理前端跨域的思路、配置和踩坑记录整理出来适合正在做前后端分离项目、被跨域问题卡住的朋友参考。跨域的根子不在前端也不在后端而在浏览器的同源策略。简单说浏览器把不同源协议、域名、端口任一不同的请求默认当成“外人”为了安全不让前端JS直接读取响应内容。而Nginx作为反向代理服务器正好卡在浏览器和后端服务之间既可以伪装成“同源”转发请求也能在响应头上手动放行跨域权限两大路线捣鼓明白了跨域基本就解决了。1. 跨域问题的本质浏览器安全模型的边界1.1 同源策略到底拦截了什么很多刚接触跨域的同事第一反应是“后端接口明明能访问为什么浏览器里就报错”。这里要理清楚后端其实收到了请求也正常返回了数据但浏览器把响应扣下了不让JS拿到。同源策略管的是——A页面里的JS只能读取A源协议域名端口的响应内容。比如前端跑在https://www.example.com:8080后端API跑在https://api.example.com端口不同就是跨域浏览器默认不放行。我把同源策略类比成小区门禁你住A栋门禁卡只能开A栋的门。你跑去B栋串门B栋保安后端其实让你进去了也给了你东西但你回了A栋A栋门卫浏览器看你手里拿的是B栋的东西直接没收。所以问题不在B栋给不给而在A栋门卫认不认。浏览器拦截的方式有两种一种是简单请求比如GET、POSTContent-Type为text/plain等浏览器直接发出去但响应返回后JS拿不到另一种是非简单请求比如带Authorization头、Content-Type为application/json浏览器会先发一个OPTIONS预检请求探路后端不返回正确的跨域头真实请求根本不会发出去。后者是实际项目中最常见的坑后面专门讲。1.2 前端跨域的主流解法与Nginx的定位前端解决跨域的方式其实不少各有各的适用场景但大部分都有明显的“妥协”成分JSONP只能处理GET请求需要后端配合返回callback包裹的脚本现在前后端分离项目里基本被淘汰开发环境代理Vite/webpack devServer proxy只在本地开发时有效上线后就没用了后端改响应头每个接口都要处理CORS头遇到多个前端域名还要动态判断逻辑容易绕进去而且改后端代码往往要排期、走流程浏览器关安全策略本地自测还行总不能要求用户也这么干。Nginx的优势在于它处在流量入口这一层所有外部请求都先经过它。在这里统一处理跨域意味着后端代码不用改、前端代码不用改、上线环境不用动只改Nginx配置就能把问题挡住。而且Nginx作为反向代理还能顺便把静态资源托管、域名转发、HTTPS证书、负载均衡这些活一起干了一个入口解决多件事运维和开发都省心。2. Nginx解决跨域的两条路线改响应头与反向代理2.1 路线一用add_header在响应头里加CORS字段Nginx最直接的做法是用add_header指令给后端返回的响应补上浏览器需要的跨域头add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization;这三行的意思很明确允许任意源访问*允许哪些HTTP方法允许前端带哪些自定义请求头。浏览器拿到这些响应头之后发现“A栋门卫”认了就放行了。但这种方式的缺点也很明显跨域头是“附加”在响应上的请求本身还是要先打到后端。如果后端接口本身有鉴权拦截比如没登录就返回401那预检请求OPTIONS到不了后端同样会卡住。更麻烦的是Access-Control-Allow-Origin: *不能配合Access-Control-Allow-Credentials: true一起用携带Cookie的场景浏览器要求Origin必须是具体的域名不能用通配符所以很多生产环境还得动态设置Origin后面细说。2.2 路线二用反向代理把“跨域请求”变成“同源请求”这是我实际项目中用得最多、也最推荐的方式。思路是让前端只跟Nginx通信Nginx再去跟真正的后端通信。比如前端域名是www.example.com后端是api.internal.com甚至局域网IP我在Nginx里配置一个/api/路径所有前端发到/api/xxx的请求Nginx都转发到http://api.internal.com/xxx。对浏览器来说它访问的始终是同源的www.example.com/api/xxx根本没“跨域”这回事自然也就不存在跨域限制。location /api/ { proxy_pass http://api.internal.com/; 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_pass结尾带不带/路径拼接结果完全不同。比如前端请求www.example.com/api/user/list配置proxy_pass http://api.internal.com/;时Nginx会去掉/api前缀把请求转发到http://api.internal.com/user/list如果proxy_pass http://api.internal.com;不带斜杠则会把完整路径/api/user/list直接拼上去。这个细节经常导致404排查半天才发现是斜杠的问题。反向代理还有个额外好处浏览器根本不知道后端真实地址后端也不直接暴露在公网安全性顺带提升了。而且以后后端拆分成多个微服务Nginx这边按路径分流就行前端完全无感。2.3 两条路线的选型建议我自己遇到跨域需求时一般遵循这样的判断场景推荐路线原因后端接口已有大量调用方不方便改代理路径响应头方案最小侵入后端无感知前后端都是自己掌控可以约好API前缀反向代理方案彻底消除跨域附带隐藏后端需要携带Cookie跨域反向代理方案优先同源请求天然支持Cookie绕开跨域限制临时解决某个外部CDN或第三方API的跨域响应头方案视第三方是否支持配置无法控制第三方时只能看对方配置需要说明的是如果后端本身已经正确配置了CORSNginx这边就不需要再做任何处理但反过来后端没配或者配错Nginx在边缘兜底也能解决问题。这就是Nginx作为网关层最大的价值。3. 几种主流场景的Nginx配置实操3.1 场景一静态页面跨域调用后端API这是最简单的场景前端是纯静态文件比如一个Vue或React项目打包后的dist目录后端是独立的API服务。直接让Nginx托管静态文件同时用location匹配API请求并代理过去server { listen 80; server_name www.example.com; root /var/www/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; } }这样前端页面和后端API都在www.example.com下从浏览器视角完全是同源请求。location /里写try_files是为了支持前端路由刷新页面时不至于404Vue Router和React Router的history模式都需要这个配置。很多人配完之后发现点击链接跳转正常、一按F5就404基本就是少了这一行。3.2 场景二响应头方案处理跨域API如果后端服务不方便用反向代理比如API域名是固定的、前后端契约已定死那就用响应头方案server { listen 80; server_name api.example.com; location / { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization, X-Requested-With, Accept; if ($request_method OPTIONS) { add_header Access-Control-Max-Age 86400; add_header Access-Control-Allow-Credentials true; 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, Accept; return 204; } proxy_pass http://127.0.0.1:8080; } }这里有个我踩过的坑add_header一旦写了Access-Control-Allow-Origin: *浏览器遇到携带Cookie的请求就直接报错因为规范要求*不能和Allow-Credentials: true共存。所以带Cookie的跨域场景我改用$http_origin动态回显请求来源相当于告诉浏览器“我确实认你Origin但我没有用通配符”两边都满足。3.3 场景三携带Cookie的跨域请求跨域带着Cookie操作是前后端分离项目最棘手的情况。反向代理方案天然规避了这个问题但如果只能用响应头方案配置就讲究一些location / { # 动态反射Origin不能用* add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization; if ($request_method OPTIONS) { return 204; } proxy_pass http://127.0.0.1:8080; proxy_cookie_path / /; # 可选调整Cookie路径 }注意几个容易翻车的地方前端XHR/Fetch请求必须设置withCredentials: true后端响应里也要明确Access-Control-Allow-Credentials: trueNginx这层如果用了add_header Access-Control-Allow-Origin *一定会炸必须换成$http_origin如果后端设置了Set-CookieNginx代理时可能会因为路径或域名不匹配导致前端存不下Cookie需要视情况用proxy_cookie_path或proxy_cookie_domain做修正。3.4 场景四本地开发环境多站点多端口配置团队开发时经常遇到这种情况每个人本机起了一套完整环境前端一个端口、后端一个端口、可能还有Mock服务、管理后台多个服务端口不一。这时候与其让前端代码里写死多个环境地址不如在本地统一搞一个Nginx入口用不同域名区分项目内部转发到不同端口server { listen 80; server_name dev.example.com; location / { proxy_pass http://127.0.0.1:3000; # 前端开发服务器 } location /api/ { proxy_pass http://127.0.0.1:8080/; # 后端服务 } } server { listen 80; server_name admin.example.com; location / { proxy_pass http://127.0.0.1:3001; # 管理后台 } }然后在本地hosts文件加上127.0.0.1 dev.example.com 127.0.0.1 admin.example.com这样每个人访问的域名都统一了前端代码里只需要一个API地址不会再出现“我本机是8080你本机是8081”这种混乱。Windows上改hosts文件要用管理员权限macOS/Linux直接改/etc/hosts就行改完如果没生效可能是DNS缓存的原因用ipconfig /flushdnsWindows或dscacheutil -flushcachemacOS刷新一下。这个方案配合压缩头部gzip和静态资源缓存本地开发体验能提升不少。4. 预检请求OPTIONS跨域的隐形杀手4.1 为什么浏览器总是先发OPTIONS只要前端用自定义请求头比如Authorization: Bearer xxx、非简单Content-Type比如application/json、或者请求方法是PUT/DELETE/PATCH浏览器就会自动先把OPTIONS请求发出去试探后端是否允许跨域。很多后端框架默认不处理OPTIONS导致预检请求直接404浏览器看到没放行真实请求就压根不发了。我之前排查过一个“偶现跨域报错”的问题现象特别迷惑同一个接口有时候通有时候不通。后来抓包发现前端某些请求带上了自定义头触发了预检而另一些请求恰好是简单请求不需要预检就通了。所以不是接口不稳定而是预检这关没过。4.2 拦截OPTIONS请求并直接返回结果在处理跨域的Nginx配置里最省事也最稳妥的做法是把OPTIONS请求“就地正法”根本不转发给后端if ($request_method OPTIONS) { 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-Max-Age 86400; add_header Access-Control-Allow-Credentials true; return 204; }return 204表示成功但没有返回体浏览器拿到这个响应再看响应头里的Allow-*字段就知道后续真实请求可以发了。Access-Control-Max-Age 86400的作用是告诉浏览器这次预检的结果可以缓存一天这一天内同样的跨域请求不再发OPTIONS能明显减少请求次数。这里有个Nginx的语法坑if指令里不能用常规的proxy_pass如果预检请求被转发到后端后端多半不会返回跨域头。所以我一般把OPTIONS的处理和实际请求的转发分开写location /api/ { if ($request_method OPTIONS) { 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; add_header Access-Control-Max-Age 86400; add_header Access-Control-Allow-Credentials true; return 204; } proxy_pass http://127.0.0.1:8080/; }4.3 是否需要保留OPTIONS的转发有些后端框架比如Spring Boot通过拦截器加CORS、Node.js的cors中间件自己会处理OPTIONS这种情况下Nginx里再拦截一次也没问题反正返回204和跨域头就行。但如果后端逻辑确实依赖OPTIONS请求极少见基本是自定义框架自己写的那就别拦截只加响应头。我的经验是优先Nginx拦截简单直接少一个请求打到后端就少一分性能压力。5. 实战中高频踩坑与排查技巧5.1 重复的Access-Control-Allow-Origin头有一次配置好后浏览器报错说“Origin多个值只允许一个”。排查了半天发现后端框架自己已经加了CORS头比如Spring的CorsFilterNginx又加了一遍两个Origin头同时出现在响应里浏览器直接拒绝。解决方案是让后端统一处理或者让Nginx统一处理二选一。如果决定Nginx来管后端就把CORS逻辑关掉反之Nginx里只保留代理转发不要加add_header。用curl验证最直观curl -i -H Origin: http://www.example.com http://api.example.com/user看返回的响应头里是否有多个Access-Control-Allow-Origin一眼就能看出来。5.2 SameSite与Cookie跨域前端在www.example.comNginx代理到后端127.0.0.1:8080虽然浏览器看前端和后端同源了但Cookie实则还是按127.0.0.1:8080域下发的。如果后端设置了Set-Cookie浏览器存储时可能带不上或者下次请求时不会自动带上。这种情况要把Nginx代理时的Cookie域和路径调整一下proxy_cookie_path / /; # 把路径重写为根路径 proxy_cookie_domain 127.0.0.1 $host; # 把域改为当前访问域名不过更推荐的做法是后端设置Cookie时不绑定特定域SameSiteNone; Secure搭配因为现在浏览器新版本对跨域Cookie限制越来越严不处理清楚就是连环坑。5.3 HTTPS反向代理的证书与Host透传Nginx挂HTTPS证书反向代理到内部HTTP服务时有个常见的“内网通、外网不通”的问题server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }X-Forwarded-Proto $scheme这一行别省。后端如果想要判断请求是HTTPS还是HTTP比如生成回调地址、校验安全来源依赖这个头才能知道外部实际是HTTPS请求否则后端一看http://回调地址拼出来就是错的前端拿到的数据里URL全不对。5.4 解决刷新页面404与强制刷新缓存处理完跨域之后前端经常会遇到两个“次生问题”一个是前面提过的history模式刷新404用try_files解决另一个是改了前端代码之后用户浏览器缓存了旧版静态资源点一遍刷新还是旧界面。我的做法是在Nginx层配合前端打包的版本号机制location /assets/ { add_header Cache-Control no-cache, no-store, must-revalidate; }前端打包时给JS/CSS文件名加上内容哈希Vite和Webpack默认行为每次发布后文件名变了自然就不会命中旧缓存。这和跨域本身没直接关系但既然都动Nginx配置了顺手把缓存策略也理一遍省得后面被用户吐槽。5.5 定位跨域问题的三板斧遇到跨域报错我推荐按这个顺序排查别一上来就盲目改配置看浏览器Network面板先确认有没有发出请求、发出的是OPTIONS还是真实请求、响应头里有哪些CORS字段、响应状态码是多少用curl模拟请求加-H Origin: http://www.example.com直连Nginx看返回的响应头排除浏览器干扰确认配置加载改完Nginx配置后用nginx -t先测语法再nginx -s reload平滑加载避免配错了半天下发不生效。nginx -t nginx -s reload很多“改了配置没反应”的情况其实是配置文件语法就错了Nginx根本没能加载成功reload之后还是旧配置。先跑一遍nginx -t是最起码的自检。6. Nginx在跨域之外还该注意的安全细节点6.1 别为了跨域把接口完全裸奔用响应头方案时有人图省事直接写Access-Control-Allow-Origin *等于告诉任何网站都能跨域读取你的接口数据。如果接口本身有鉴权还好一旦哪个接口漏了鉴权别人网站里的恶意脚本就能把你的接口当免费数据源用。更稳妥的做法是用$http_origin做白名单判断。Nginx可以通过map指令实现map $http_origin $cors_origin { default ; ~^https://(www\.)?example\.com$ $http_origin; ~^https://admin\.example\.com$ $http_origin; } server { location /api/ { add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials true; if ($request_method OPTIONS) { return 204; } } }这样只有example.com和admin.example.com的页面能跨域获取数据其他来源拿不到跨域头。6.2 隐藏Nginx版本与关闭无用模块信息很多安全扫描工具会通过HTTP头里的Server: nginx/1.24.0识别版本号再针对性找漏洞。虽然不是跨域问题但和跨域配置一起做正合适一条指令搞定server_tokens off;另外跨域放行了并不意味着后端该暴露的接口都暴露了。比如服务器上的文件访问如果走Nginx代理location里要小心alias路径拼接错误否则别人可能通过构造路径读到不该读的文件。我的经验是文件下载类的代理尽量用固定的文件目录避免用户可控制的参数拼路径。6.3 禁止爬虫与防止源码被直接查看前端项目如果不想被爬虫收录或通过源码分析接口结构Nginx层面也能做一定拦截比如禁止非浏览器UA访问敏感路径location ~* \.(git|svn|env|log)$ { deny all; } if ($http_user_agent ~* (python-requests|curl|wget|scrapy) ) { return 403; }不过这东西防君子不防小人真正的爬虫会伪装UA属于基础加固不能依赖它达到绝对安全。核心接口该做的鉴权和限流还是要后端好好做。7. 补充几个Nginx跨域配置的实用细节7.1 add_header与继承关系的理解Nginx里的add_header有个容易忽略的规则如果在某个location里写了自己的add_header那它不会继承上一级server里的add_header除非加了always且处理好继承细节。所以我在实际配置时要么把所有跨域头都在最顶层写统一要么在每个location里写全避免“明明配了但没生效”的困惑。7.2 用include拆分配置文件跨域配置如果散落在多个server块里又长又乱。我会把公共的CORS配置提出来单独存为一个文件然后在需要的地方include进来# /etc/nginx/conf.d/cors.inc add_header Access-Control-Allow-Origin $http_origin always; 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;使用时server { location /api/ { include cors.inc; proxy_pass http://127.0.0.1:8080/; } }这样一行include就能把通用跨域头带进来维护起来轻松多了。要注意的是include的文件路径必须放在Nginx能读到的地方权限也别搞得太开放毕竟这算网站入口配置的一部分。7.3 动态Origin与静态Origin的选择如果在公司内网、控制严格的环境里所有前端域名都是已知的直接写死多个允许的Origin也行性能最好。但如果前端域名经常动比如多级测试环境、动态域名务必用$http_originmap方式动态反射这样能避免开发环境频繁改配置。我在外网项目上踩过一次教训认为前端域名固定在配置里写死了Access-Control-Allow-Origin: https://fixed.example.com结果市场部临时做了个落地页域名接口全部跨域失败大晚上的还得爬起来改Nginx。从那以后只要是给外部用的接口一律$http_origin 白名单再也没出过类似的紧急事故。8. 结束语Nginx跨域的经验之谈跨域的坑我在不同项目里踩了不止一遍但用Nginx处理得越久越觉得它不只是“前端问题的土办法”而是整个前后端协作架构里很重要的一层。改业务代码解决跨域往往只能管一个项目在Nginx层面解决整个环境的所有项目都能受益后端不用动前端也不用为不同环境写一堆判断逻辑。我个人现在处理跨域问题的习惯是先用curl确认后端到底有没有正常返回数据确认是跨域拦截后优先走反向代理其次是响应头方案配置改完必须nginx -t验证再reload最后在浏览器里看Network确认预检请求和请求头都符合预期。这套流程虽然简单但几乎覆盖了90%以上的跨域问题。最后送一个实用小技巧配置里给location加上expires、gzip等静态优化项配合跨域配置一起管理会让整个网站在交付后跑得更稳这些问题虽然看起来和跨域无关但所有Nginx配置放在一起统一治理才是真正省心的长期方案。