Nginx反向代理解决前端跨域:从原理到实战配置全解析
Nginx解决前端跨域算是老生常谈但又绕不开的话题。本地开发联调、测试环境部署、上线前的接口联调几乎每个前端都会撞上跨域报错。我这些年经手过不少前后端分离的项目从最早的JSONP到后来的CORS配置再到最后统一用Nginx反向代理一劳永逸踩过的坑比文档里写的多得多。这篇就把Nginx解决跨域的完整思路、配置方案、以及那些文档里不会写的坑一次性理清楚。1. 跨域问题的本质与常规解法1.1 同源策略到底限制了什么浏览器之所以有跨域这个概念根源在于同源策略。所谓同源指的是协议、域名、端口三者完全一致任何一个不同浏览器就会拦截跨域请求的响应。很多刚接触前端的同学以为跨域是后端接口不支持其实恰恰相反请求往往已经发出去了服务器也处理了但浏览器拿到响应后发现非同源直接给拦下来不交给前端代码。举一个生活化的例子你住在小区A想找小区B的物业开个证明。你亲自跑过去人家把证明开好了递给你但小区A的保安不认这份证明说你不是B小区的业主直接扣下不让你带进去。整个过程中物业其实已经办完事了卡住你的是你自家门口的保安也就是浏览器。同源策略是浏览器的一种安全机制它防止恶意脚本跨域读取数据。但在前后端分离的开发模式下前端页面部署在一个域名后端API在另一个域名跨域就成了常态必须想办法绕开这一限制。1.2 常见跨域方案的适用边界业内处理跨域有几条常规路线CORS是正统方案后端在响应头里加上Access-Control-Allow-Origin等相关字段告诉浏览器“我允许这个源访问”。配置灵活支持各种请求方式但需要后端配合改代码而且预检请求、自定义头、cookie携带这些问题处理起来稍显繁琐特别是当接口数量多、后端组织架构复杂时推动成本不低。JSONP是古早方案利用script标签不受同源策略限制的特性动态插入script标签加载带回调参数的接口地址。但它只支持GET请求不支持POST等其它方法在当今RESTful风格接口盛行的背景下局限性很大只能作为特定场景的补充方案。反向代理方案则是前端不动后端不动在中间加一层Nginx做请求转发。浏览器只同源访问NginxNginx再向后端服务转发请求然后把响应返回给浏览器。因为浏览器和Nginx同源所以没有跨域问题Nginx是服务端程序自己访问后端不受同源策略约束。这也是我最终推荐多数项目使用的方式。顺带一提还能顺带解决开发环境和生产环境接口地址不一致带来的配置切换问题一举多得。还有基于Node.js的中间层代理思路类似但多了一层Node服务部署和运维成本高一些适合有BFF层的团队。整体来看Nginx反向代理实战中最常用配置量小、改动少、见效快。2. 方案选型为什么优先推荐Nginx反向代理2.1 反向代理的核心原理反向代理的实质是“请求中转站”。客户端浏览器请求NginxNginx把请求转发给后端真实服务器后端响应后Nginx再把数据原样返回给客户端。在这个链路里浏览器只知道自己在和Nginx通信根本不知道后端服务器的存在。因为前端页面和Nginx域名、端口都一致满足同源策略浏览器自然不会有任何拦截动作。从协议层面看Nginx的转发行为发生在HTTP请求层面它对浏览器而言是服务端对后端而言是客户端。服务端之间的通信不涉及浏览器的同源策略这是整个方案成立的基础。之前有个项目前端部署在某云主机后端API在另一台机器的8080端口开发环境用本地node服务代理测试环境每次发布都要手工改前端配置繁琐且容易漏改。后来统一在Nginx里配了location转发规则环境切换只需改Nginx的upstream地址前端代码里压根不需要配置接口域名彻底解决了这个问题。2.2 与纯CORS方案对比各有什么优劣有些团队习惯用CORS解决跨域后端在代码里加一个cors过滤器一劳永逸。但实际用下来Nginx方案有几个优势是CORS比不了的一是无需改动后端代码。很多项目的后端不是自己能掌控的可能由别的团队维护甚至部分是老系统外包遗留。推动别人改代码往往要排期、要联调、要回归而Nginx配置是运维侧的事前端加一条转发规则就能搞定效率立竿见影。二是规则统一好维护。多个前端业务比如管理后台、H5页面、小程序后端都汇聚到同一套Nginx反向代理上接口路径统一以/api开头Nginx里按前缀分流后续新增环境只需改一处配置文件reload即可不会出现各个前端代码里散落各自接口域名的情况。三是隐藏真实服务端节点。反向代理转发到后端内网地址外部无法直接触达后端服务器减少暴露面。虽然这不算跨域问题的核心但在实际部署中确实是加分项。CORS方案也并非没有优点它对开发者透明调试方便且适合跨多个不同域的开放接口。但如果是自有系统前后端联调我还是推荐Nginx。2.3 常见Nginx部署模式实际部署中有两种常见拓扑第一种是前后端分离部署。前端静态资源放在Nginx站点目录后端API跑在应用服务器指定端口Nginx同时承担静态文件服务和API反向代理两个职责。第二种是前后端完全分离部署比如前端单独放CDN或另一台服务器Nginx只负责反向代理API接口。这种情况下前端页面请求 /api 开头的接口浏览器访问的域名是前端域名该域名在Nginx配置里配了转发规则同样可以解决跨域。无论哪种模式Nginx配置的核心逻辑是一致的匹配请求路径转发到upstream指定的后端地址返回响应。差异只在于是否同机托管了前端静态文件。3. 核心配置拆解一份能直接上线的Nginx跨域配置3.1 最简配置单location转发假设后端服务跑在 http://inner-server:8080 前端部署在同一台Nginx上需要让浏览器请求 /api 前缀时转发到后端。server { listen 80; server_name example.com; location /api/ { proxy_pass http://inner-server: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; } }这一段虽然简单但有两处细节必须注意。proxy_set_header Host如果不设置后端收到的Host会变成inner-server:8080部分后端框架在生成绝对URL或做重定向时会出问题。X-Forwarded-For如果不设置后端日志里看到的客户端IP全是Nginx的IP排查问题会绕远路。更关键的是 location 和 proxy_pass 的路径拼接规则。location是 /api/proxy_pass的URI部分是 http://inner-server:8080 末尾没有路径那么浏览器请求 /api/user/list 时Nginx会完整转发为 /api/user/list 到后端。如果后端接口本身没有 /api 前缀就需要调整。3.2 去掉前缀的转发配置大多数情况下后端接口路径是 /user/list 只有一个服务的话后端不需要 /api 前缀这时配置改为location /api/ { proxy_pass http://inner-server:8080/; }注意proxy_pass末尾的 /。有斜杠和没斜杠Nginx的拼接行为完全不同proxy_pass http://inner-server:8080; 无斜杠→ /api/user/list 原样转发proxy_pass http://inner-server:8080/; 有斜杠→ /api/user/list 去掉 /api 后转发为 /user/list这一类问题在网上的讨论帖里出现频率极高。很多新手照着配了之后发现404就是路径拼接规则没搞清楚。记住一个口诀proxy_pass末尾带不带斜杠决定了原URL里location匹配部分是否被替换。带斜杠即用斜杠替换匹配的location前缀不带斜杠则把完整原始URI追加到proxy_pass地址后面。3.3 多环境多服务的完整配置模板实际项目往往不止一个后端服务可能包含用户服务、订单服务、文件服务等。我会按前缀分流upstream user_service { server 192.168.1.10:8080 max_fails3 fail_timeout30s; } upstream order_service { server 192.168.1.11:8081 max_fails3 fail_timeout30s; } server { listen 80; server_name api.example.com; # 用户服务 location /api/user/ { proxy_pass http://user_service/; proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 30s; } # 订单服务 location /api/order/ { proxy_pass http://order_service/; proxy_connect_timeout 5s; proxy_read_timeout 30s; } # 通用兜底转发 location /api/ { proxy_pass http://user_service/; } # 静态资源如果前端也在这台机器 location / { root /var/www/dist; index index.html; try_files $uri $uri/ /index.html; } }upstream块在这里不仅定义了后端地址还支持负载均衡配置。比如同一个服务部署了两台机器可以写多个serverNginx默认轮询分发请求。max_fails和fail_timeout配合使用当某台机器连续失败达到阈值Nginx会在一段时间内不再把请求分给它这对保障联调环境稳定性很有帮助。虽然项目初期可能只有一台后端但提前把upstream结构定下来后面扩容机器时只需要在upstream里加一行不必动location规则。注意兜底location /api/放在最后。Nginx匹配location遵循前缀匹配的最长优先原则/api/user/ 比 /api/ 更长所以用户请求会精确命中上面的规则不会落到兜底转发里。我把兜底指向user_service防止前端请求了未单独配规则的接口时直接404方便排查。3.4 处理带cookie的请求跨域请求中cookie是最容易出问题的点。默认情况下浏览器不会跨域携带cookie。Nginx反向代理从源头上规避了跨域但也要留意cookie的传递。前端页面和Nginx同域cookie天然能携带。Nginx转发到后端时如果后端校验cookie需要保证Host和cookie的域匹配。配置里我习惯加一行proxy_set_header Cookie $http_cookie;这行代码把浏览器传来的cookie原样转发给后端。如果后端用了不同的cookie name还可以用proxy_set_header做改写。不过一般情况下只需要保证同域会话即可后端返回Set-Cookie时由于浏览器看到的是Nginx的域名cookie会种到Nginx域下后续请求自然带上问题不大。有个特殊场景是部署子路径时cookie的path属性可能会影响携带。比如cookie种的path是/前端在子路径下也能带上如果后端Set-Cookie的path是/api/user那只有访问该路径前缀的请求才会携带。这种问题排查起来很隐蔽如果遇到登录状态异常建议先看浏览器Application面板里cookie的path和domain。3.5 配置确认与重载写完配置先用nginx -t检查语法再平滑重载nginx -t nginx -s reloadnginx -t会输出配置文件的语法检查结果。如果看到syntax is ok说明配置没问题。注意reload不会中断现有连接是平滑的可以放心在生产环境执行。如果机器上同时有多个配置文件子目录注意确认主配置文件nginx.conf里include了conf.d下的文件。我之前接手过一个项目改了半天的conf.d/xxx.conf但nginx -t硬是没报错请求却全走默认规则排查半天才发现主配置根本没include那个文件教训深刻。3.6 关于CORS响应头的一种特殊处理某些场景下比如前端在开发环境用webpack-dev-server的proxy或者临时起了一个静态服务器没法快速改Nginx配置但又确实需要跨域访问某个已有Nginx站点上的API。这时候可以给Nginx开启CORS响应头支持location /api/ { add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS always; add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Max-Age 1728000 always; if ($request_method OPTIONS) { return 204; } proxy_pass http://inner-server:8080; }这里有个小技巧add_header指令默认只在响应码为200等成功状态时生效加always参数表示所有响应都加上响应头。如果不加always遇到404或302时响应头会消失前端照样报跨域错误。$http_origin表示动态读取请求的Origin头来设置允许的源比写死Access-Control-Allow-Origin:要安全因为配合allow-credentials时不允许使用。用if判断OPTIONS请求直接返回204是为了处理CORS预检。预检是浏览器在发起跨域请求前先用OPTIONS探一下服务器允不允许。如果Nginx光转发到后端后端没处理OPTIONS预检就失败了真正的请求根本不会发出。加了这个判断预检请求在Nginx层直接返回成功一步到位。4. 实操过程中最常遇到的5个坑与排查总结4.1 刷新页面404典型场景前端项目用history模式路由用户直接访问 /user/profile 或刷新页面时Nginx返回404因为Nginx只在 / 路径下找到了index.html而 /user/profile 没有对应的静态文件。这个问题的根源在于单页应用的路由是前端在浏览器端模拟的服务器上并不存在 /user/profile 这个真实路径。配置里加上try_files即可location / { root /var/www/dist; index index.html; try_files $uri $uri/ /index.html; }try_files会依次尝试按当前URI找文件、找目录都找不到就回退到/index.html由前端路由接管解析。注意一定放在location /块下不要误放在/api/里否则API请求都会被重写到index.html接口全挂。这是我在新项目上踩过的真实教训前后端联调半天后才发现所有请求都返回了HTML。4.2 配置正确但还是跨域报错出现CORS error但Nginx配置看着没问题大概率是浏览器缓存导致。旧响应头被浏览器缓存新配置未生效尤其是一些框架上线后静态资源带hash但index.html被CDN或浏览器强制缓存时前端连Nginx域名可能都缓存的旧版本。排查思路先强制刷新再查看网络请求的响应头里有没有预期的响应头字段确认是不是走了CDN或旧缓存。如果页面由CDN加速记得在CDN层刷新缓存或验证CDN是否透传了响应头。另外Nginx的add_header在上一级server配置了但在location里代理转发后可能没有继承需要在本级再声明一次这是Nginx的继承机制易踩的点。4.3 404 vs 502分清是Nginx路由问题还是后端挂了看到404先判断是不是路径拼接问题location /api/ 和proxy_pass结尾有没有斜杠后端是否真的存在该路径。看到502 Bad Gateway说明Nginx能匹配到location但转发时连不上后端。检查upstream里配置的IP和端口是否正确用curl从Nginx所在机器直接访问后端地址测试连通性再确认后端服务是否启动、是否有防火墙拦截。这个分类在排障中极其有效能大幅缩小问题范围。我接手了一个跨域问题工单前端同学说“Nginx配置没问题啊”结果我一条curl命令就发现后端8080端口只监听在127.0.0.1上外部访问不通Nginx自然502。4.4 刷新几次成功几次失败WebSocket模式该注意什么项目涉及WebSocket时Nginx反向代理需要额外配置升级头因为WebSocket协议的握手依赖Upgrade和Connection两个请求头。配置如下location /ws/ { proxy_pass http://inner-server:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 60s; }不配这个WebSocket连接会在握手阶段失败或者连接一会儿就断开。proxy_http_version 1.1也很关键HTTP/1.0不支持Upgrade头。另外长连接场景下proxy_read_timeout需要调大否则60秒没有消息通信Nginx就主动断开连接前端会莫名其妙收到onclose事件。前端同学排查这种问题往往会走弯路以为是自己心跳逻辑写错了实际是Nginx断的。4.5 上线后接口能通但登录态丢了排查方向先确认cookie的domain和path。如果后端返回的Set-Cookie的domain写的是后端域名浏览器种cookie时会因为与当前页面的域不一致而丢弃或种下来但后续请求带不上。Nginx的proxy_cookie_domain指令可以把Set-Cookie里的domain改写为前端域名proxy_cookie_domain inner-server.example.com example.com;同时注意Secure标记。如果Nginx提供的是HTTPS且cookie带Secure属性浏览器只会通过HTTPS请求携带它。若某个内部页面用HTTP访问cookie不会带上登录态就莫名其妙丢了。如果想深挖可以在浏览器Application面板看cookie的Secure列排查很快。但一般来说反向代理同域部署后登录态问题集中在cookie domain/path两处逐一核对即可。我见过最隐蔽的一个case是后端框架会自动把Set-Cookie的domain写成当前请求的Host而proxy_set_header Host如果没设整个链路cookie域就乱了表现就是偶尔能登录偶尔不能搞了半天才发现是Host头没传。5. 配置落地之后我个人的几点心得跨域问题看着是个小点却牵扯到前端、后端、运维三条线的协作。Nginx方案最省心的地方在于它把跨域问题转化为一个单纯的请求分发问题前端不用改代码后端不用改代码只在接入层统一处理规则清晰、可审计、可回滚。我对新项目的最低配置要求是预留upstream块、统一/api前缀、配置好正向代理三件套Host、X-Real-IP、X-Forwarded-For、尝试前端history路由的try_files。这四样做好后续联调和上线的摩擦成本能降一大半。如果你还在用改后端加CORS头的方式应付跨域而这个系统后端不是自己团队维护的话建议尽快推动Nginx反向代理方案落地。把规则握在自己手里不只是省事更是在后续的环境迁移、域名切换、灰度发布中给自己留下更从容的操作空间。