资讯动态

用Nginx反向代理解决前后端分离部署跨域问题

发布时间:2026/9/19 8:31:04 来源:尧图企业网站定制
前后端分离项目第一次部署到云服务器上宝塔、Nginx、Node 这三样东西要是搭配不好跨域问题能让人怀疑人生。前端是 Vue、后端是 Node听起来就是常规操作结果我在跨域这个坑里卡到凌晨。本地开发时前端跑 8080、后端跑 3000Vite 里配个 proxy 就能联调到了生产环境前端打包后放到 Nginx后端 Node 服务用 PM2 拉起浏览器一打开页面接口请求全被拦控制台一片 CORS error。最终我跳出了“在代码里改跨域”的思路改用 Nginx 反向代理把前后端收敛到同一个域名下问题才算彻底解决。下面我就把这套宝塔面板 Nginx Node 的部署过程完整拆开讲清楚跨域为什么会出现、怎么从架构上根治适合第一次独立做部署的开发者也适合被生产环境跨域反复折磨的同行。1. 跨域问题到底卡在哪先搞清楚浏览器在拦什么1.1 浏览器眼里的“同源”比你想的严格很多人一看到 CORS 报错就急着往后端塞跨域中间件这样运气好能解决但大多数时候越改越乱。先花两分钟理解同源策略后面所有配置就都有依据了。所谓同源是指协议、域名、端口三者完全一致。页面地址是 https://www.example.com接口地址是 http://123.123.123.123:3000协议不同、域名不同、端口也不同浏览器直接判定为跨域。哪怕你只是把前端从 443 端口挪到 3000 端口浏览器照样拦因为端口不同。这里有个非常关键的认知跨域拦截发生在浏览器这一层后端接口大概率是正常收到了请求也执行完了只是浏览器不允许前端 JS 读取响应。很多人看到 CORS error 以为服务挂了其实服务活得好好的。你直接在浏览器地址栏访问那个接口通常能看到 JSON就是因为地址栏访问天然不受同源策略约束。1.2 三种解法里为什么生产环境我选 Nginx代码层面解决跨域常见三招。第一招 CORS由后端在响应头里加 Access-Control-Allow-Origin告诉浏览器这个接口允许你读。它的优点是灵活适合对外开放的 API 平台缺点是生产环境要处理预检请求 OPTIONS、多域名白名单、Cookie 携带规则一多就容易顾此失彼。第二招 JSONP借助 script 标签可以跨域引用的特性取数据实现简单但只支持 GET现在已经边缘化。第三招 Nginx 反向代理让外部请求统一打到同一个域名再按路径把请求转发给后端前端代码里只用相对路径 /api浏览器看到的是同源请求跨域从根上消失。我推荐生产环境优先第三招主要是两个原因。一是前后端代码几乎不用为跨域写额外逻辑以后后端换机器、换端口只改 Nginx 配置业务代码一行不动二是后端端口可以不暴露公网少一个攻击入口。本地开发时用 Vite 或 Webpack 的 proxy思路和 Nginx 完全一样都是多找一道转发让浏览器以为世界上只有一个服务。方案主要改动位置优点要注意的点CORS 响应头后端灵活适合开放 APIOPTIONS 预检、域名白名单、Cookie 携带JSONP前后端都改简单只支持 GET已边缘化Nginx 反向代理服务器配置代码改动最小、同源访问需要会配 Nginx路径要规划好2. 部署前的基础环境宝塔、Node、Nginx 三件事的顺序我见过不少新手一上来就装环境装到一半端口占用、版本冲突最后只能重装系统。正确的顺序是先把宝塔面板立起来再装 Nginx然后处理 Node 环境最后才轮到项目代码。目标架构一句话就能讲清楚Nginx 对外提供前端页面、转发后端接口Node 只在本机跑接口服务宝塔面板负责把 Nginx、证书、文件管理这些琐事管起来。2.1 宝塔面板装完先做的不是传代码而是安全设置在全新服务器上执行宝塔官方安装脚本具体命令以官网当前文档为准因为不同系统版本偶尔会调整。装完后会打印面板地址、初始账号和密码这一串信息先存到本地丢了很麻烦。然后立刻做三件事第一改掉面板默认端口默认 8888 太容易被扫描改成不常用的端口第二把面板密码换成足够复杂的强密码有条件就给面板绑定域名并加访问限制第三到云服务商控制台检查安全组或防火墙把 80、443 和面板端口放行。这一步最容易漏我第一次部署时 Nginx 装了、网站建了域名就是打不开排查半天发现是安全组没放行 80典型的自摆乌龙。2.2 Node 环境用 nvm 管版本别让版本成为第一个坑宝塔软件商店里有 Node.js 版本管理器可以用面板按钮装操作直观。我自己更习惯在命令行装 nvm原因是它对多版本切换说一不二适合一台服务器上同时维护好几个项目的场景。安装 nvm 和指定版本的 Node基本就是下面几条命令curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 18.20.4 nvm use 18.20.4 node -v npm -v版本号可以按项目要求调但有一点必须坚持服务器上的 Node 大版本和本地开发保持一致。我在生产环境踩过最典型的坑是本地 Node 18 跑得好好的服务器上装的是旧版本一启动就语法报错。还有一些带原生模块的依赖比如 bcrypt、sharp版本不对直接编译失败报错信息里全是 gyp 错误。遇到这种问题先别急着排查代码先用 nvm 切回项目要求的 LTS 版本再装依赖往往就顺了。2.3 Nginx 的双重身份决定了一切配置思路在宝塔软件商店里安装 Nginx新手阶段别同时装 Apache避免两个服务抢 80 端口虽然未必冲突但没必要给自己加戏。之后在面板的网站功能里添加站点时PHP 版本选纯静态因为我们前端只是一堆打包好的 html、css、js不需要 PHP 执行。宝塔建站本质上就是帮你生成一份 Nginx server 配置把域名、根目录、日志都安排好后续我们只需要在这份配置里加接口转发规则。理解 Nginx 的 location 匹配机制能让排错少走弯路。一个请求进来后Nginx 先按 server_name 找到对应的 server 块再在 server 里根据路径去匹配 location。location / 是兜底规则location /api/ 因为前缀更长更具体所以请求 /api/login 会优先命中它。这也是为什么我建议所有后端接口统一挂 /api 前缀前端请求只用相对路径Nginx 一条规则就把所有后端请求接管了。3. 后端 Node 服务部署从“能跑”到“一直能跑”3.1 上传代码前先过一遍本地上线清单部署后端最怕的不是代码有问题而是本地跑得通、上服务器就崩。所以在传代码之前我会先过一遍清单。第一项目在本地要以生产模式完整跑通一次不是开发模式第二数据库地址、密钥、回调地址这类敏感配置不要写在源码里放到 .env 或其他环境变量文件第三确认项目有 package-lock.json保证服务器安装依赖时的版本和本地一致第四想清楚后端路由有没有 /api 前缀这直接关系后面 Nginx 的 proxy_pass 怎么写。代码传到服务器可以放在 /www/wwwroot/backend 目录用宝塔文件上传、SFTP、git 拉取都可以。注意不要把本地 node_modules 整个传上去因为有些原生依赖和当前系统深度绑定传上去轻则白搭重则出现莫名其妙的段错误。正确做法是到服务器项目目录重新安装依赖比如执行 npm ci它的作用是严格按照 package-lock.json 安装比 npm install 更可靠。依赖装好、环境变量配好之后先在前台跑一次cd /www/wwwroot/backend npm ci cp .env.example .env # 根据生产环境修改 NODE_ENVproduction node app.js看到监听日志后立刻用 curl 验证curl http://127.0.0.1:3000/api/ping。如果这里返回了 JSON说明后端本身没问题可以进入下一步如果这一步失败后面的 Nginx 转发再完美也没用排查顺序绝对不能乱。3.2 用 PM2 让 Node 进程一直活着前台跑 node app.js 有个致命问题SSH 一断服务就没了。要么用 nohup 挂后台要么用 systemd 管但最省心的还是 PM2日志、重启、开机自启一套全包。安装和启动的命令也不复杂npm install -g pm2 pm2 start app.js --name backend pm2 status pm2 save pm2 startuppm2 start 后面的 --name backend 是给进程起名字之后看日志、重启都用这个名字。pm2 save 是把当前进程列表保存下来pm2 startup 会生成一条系统开机自启命令把命令输出里那行复制到终端执行一遍这样服务器重启后 PM2 会自动恢复所有进程。更新代码后的常规操作是 git pull、重新装依赖、然后 pm2 reload backend最后用 pm2 logs backend 看一眼有没有报错。如果 reload 后接口一直 502先 pm2 status 确认进程状态是不是 errored再看日志绝大多数问题是依赖没装全或者 .env 没配置。3.3 端口规划后端可以不向公网暴露生产上有一种常见错误是把 Node 服务监听在 0.0.0.0:3000然后在云安全组里把 3000 也放开了美其名曰方便调试。结果就是别人能直接访问 http://ip:3000跨域问题又多了一条暴露路径端口还有被扫的风险。我的做法是让后端监听 127.0.0.1:3000只有本机 Nginx 能转发进去公网根本无法直接访问。如果代码里没法改绑定地址那就必须在云安全组和服务器防火墙里只放行 80、4433000 不对公网开放。端口收紧之后还有一个附带好处用户在浏览器里的所有请求都走同一个域名跨域问题从访问路径上就被堵死了。CORS 那套代码保留了也没关系但基本不会触发相当于上了一道双保险。4. 前端打包上线build 之前得想清楚这三件事4.1 接口地址统一改成相对路径 /api前端打包前最重要的一件事是把所有接口请求的基础地址改成相对路径 /api。以 axios 为例// request.js import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) export default request请求发出之后浏览器看到的是 https://www.example.com/api/login和页面同源不会触发跨域。服务器上的 Nginx 再把 /api/login 转发给 Node。如果代码里到处都是 http://ip:3000 这种绝对地址上线之后跨域报错一点不冤。改的时候全局搜 baseURL、VITE_API_BASE、axios.defaults.baseURL 这些配置项统一收敛到一个文件里。有人担心改成相对路径后本地联调怎么办。本地开发时用 Vite proxy 把 /api 转发到本机 3000生产环境用 Nginx 转发到服务器 3000代码里始终只出现 /api一套代码两个环境都能跑。这也是我理解的前后端分离工程化该有的样子。4.2 前端路由千万留意 history 模式的刷新 404如果前端用了 Vue Router 的 createWebHistory或者 React Router 的 BrowserRouter部署后会出现一个隐蔽问题用户停留在 /login 页面按一下 F5Nginx 会在磁盘上找 login 这个文件找不到就返回 404。这不是接口挂了是历史记录模式的刷新路径不被 Nginx 认识。解决办法是在 Nginx 的 location / 里加一行 try_files $uri $uri/ /index.html;意思是先找真实文件找不到目录就找 $uri/再找不到就回到 /index.html由前端路由接管。这样前端刷新什么路径都能回到应用。如果你图省事用 hash 模式地址变成 /#/login刷新确实没问题但 URL 不好看能上 history 还是尽量上。4.3 在宝塔里建站把 dist 内容放对位置宝塔面板左侧的网站菜单点添加站点域名填 www.example.com有需要可以把 example.com 一起加上PHP 版本选纯静态。根目录默认是 /www/wwwroot/www.example.com保持默认就行。前端打包后生成 dist 文件夹把 dist 里的 index.html、css、js 等文件上传到站点根目录注意是 dist 里面的内容不是整个 dist 文件夹再套一层。上传完成后访问域名页面应该能正常打开接口这时大概率还是 404 或 502因为 Nginx 转发还没配这是正常现象接下来才是重头戏。5. Nginx 反向代理配置跨域问题的根治方案5.1 核心原理把两个服务伪装成一个服务用户在浏览器地址栏只看到 https://www.example.com这是一个服务。但实际上页面文件在 Nginx 管理的静态目录里接口跑在 Node 进程里。Nginx 收到请求后做分流路径不是 /api 开头的直接返回静态文件路径是 /api 开头的转发到 http://127.0.0.1:3000。对浏览器来说所有内容都来自同一个源CORS 根本没有出场机会跨域从架构上消失了。这个思路和本地开发环境里的 proxy 一模一样。也正因为如此我始终觉得生产环境优先用 Nginx 做同源收敛而不是把跨域治理全寄托在后端 CORS 配置上。CORS 适合对外开放的接口服务但对我们自己搭的前后端分离项目来说Nginx 反向代理是更贴近常规 Web 架构的做法。5.2 一份可以直接抄的 server 配置下面这份配置是压箱底模板绝大多数中小项目直接改域名和路径就能用server { listen 80; server_name www.example.com; root /www/wwwroot/www.example.com/dist; index index.html; client_max_body_size 20m; 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; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 60s; proxy_read_timeout 60s; } }root 指向前端文件根目录index index.html 是默认首页。client_max_body_size 20m 是为了上传文件Nginx 默认只允许 1MB做图片上传的后台不加这一行就会出现 413 错误。location / 里的 try_files 就是给 history 路由兜底。location /api/ 是跨域解药的关键proxy_set_header 这几行把用户真实 IP、原始 Host、协议透传给 Node后端做登录日志、限流、回调地址拼接的时候都要用。改完配置先执行 nginx -t 检查语法没问题再 nginx -s reload。不要每次改完都直接 reload万一配置写错服务直接挂掉影响线上。5.3 proxy_pass 带不带斜杠一个字之差结果天差地别这是 Nginx 配置里最容易被忽略、也最容易让人抓狂的细节。如果写成 proxy_pass http://127.0.0.1:3000;也就是不带斜杠请求 /api/login 到了后端仍然是 /api/login。如果写成 proxy_pass http://127.0.0.1:3000/;也就是带斜杠Nginx 会把 location 匹配到的 /api/ 前缀替换成 /请求 /api/login 到了后端会变成 /login。前端请求proxy_pass 写法后端拿到的路径/api/loginhttp://127.0.0.1:3000/api/login/api/loginhttp://127.0.0.1:3000//login具体怎么选看后端路由定义。后端如果是 app.use(/api, routes)那用不带斜杠的写法后端如果只有 app.get(/login)用带斜杠的写法。我最怕的是前后端各加一层前端发 /api/login后端路由只定义 /login又用了不带斜杠的写法后端收到的路径是 /api/login匹配不上返回 404。所以遇到接口 404第一件事是去后端日志里看收到的路径到底是什么别急着改前端。5.4 HTTPS 和 WebSocket 一起收个尾配置跑通后别忘了把 HTTPS 补上现在主流浏览器的安全提示已经把 HTTP 页面标记得很吓人了。宝塔面板的 SSL 功能可以申请免费证书申请完勾选强制 HTTPS面板会自动把 80 端口的请求跳转到 443。证书续期一般也是自动的但偶尔会失败建议每隔一段时间看一眼到期时间。如果项目里有 WebSocket比如即时通知、在线状态这类功能单纯套之前的配置会连不上需要在 location 里加两条头location /ws/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }没有这两行WebSocket 的握手会被 Nginx 拦在门外前端控制台报的错又很不直观排查起来相当费劲。6. 验证与排错值得长期收藏的排查顺序6.1 用 curl 分两层定位别在浏览器里猜谜线上出问题我从来不直接看浏览器控制台就下结论。先用 curl 从服务器本机把链路拆成两段测。第一段测 Node 服务本身curl http://127.0.0.1:3000/api/ping通了说明后端活着。第二段测 Nginx 转发curl https://www.example.com/api/ping通了说明外网链路和转发都正常。第一段通、第二段不通问题基本锁定在 Nginx 配置第一段就不通那先去看 PM2 进程状态。浏览器控制台的信息当然要看但更适合做进一步确认。打开 Network 面板看实际请求 URL 是 https://www.example.com/api/login还是 http://ip:3000/api/login。如果是后者说明前端代码还在用绝对地址或者用户访问的根本不是一个域名CORS 报错自然会出现。这时候改代码比改 Nginx 有效。6.2 高频异常速查表现象最可能原因检查顺序页面打开接口 404前端请求路径和后端路由对不上或者 proxy_pass 斜杠写错先看 Network 里的请求 URL再看后端收到的路径最后核对 location 和 proxy_pass502 Bad GatewayNode 进程没起来或 proxy_pass 端口不对pm2 status再 curl 127.0.0.1:3000最后看 proxy_pass 端口刷新子路由 404history 路由没配 try_files检查 location / 里的 try_files上传文件 413Nginx 上传大小限制在 server 块加 client_max_body_size请求超时 504后端处理慢或 proxy_read_timeout 太短看后端日志耗时适当调大 proxy_read_timeout控制台还在报 CORS实际请求走了非同源地址检查前端 baseURL 和用户访问域名是否一致这张表对应的是我实际调试中最常遇到的六类现象。404 的问题根源多半不在 Nginx 而在路径拼接前后端只要出现基础路径不一致就会拿到这个状态码502 的问题反过来后端进程本身不健康Nginx 想转发也没得转所以优先查进程而不是查配置刷新 404 几乎可以无脑检查 try_files413 和 504 又分别指向上传大小和超时时间两个配置项。逐条按表排查大部分问题都能在十分钟内定位。6.3 上线后的日常维护进程、日志、证书三件套部署完不代表一劳永逸。我的固定习惯是三件事看进程看日志看证书。每次发布后端代码pm2 reload backend 之后马上 pm2 status 扫一眼再 pm2 logs 确认没有异常堆栈。Nginx 日志默认写在 /www/wwwlogs 目录宝塔面板里按域名分文件出现诡异问题先翻 error.log比什么都管用。证书到期这件事免费证书一般 90 天自动续期偶尔会失败到期前一周如果没收到通知就手动在面板里续一次别让用户在浏览器里看到大红屏。7. 被坑过之后我养成的几个固定动作7.1 目录和命名的约定比想象中重要服务器上的目录结构我固定成一套前端放在 /www/wwwroot/域名 目录后端放在 /www/wwwroot/backend 目录接口统一走 /api 前缀Node 进程统一叫项目名。这样做的直接好处是无论过多久回去维护不需要回忆当初是怎么安排的。配置里的路径、进程名、日志名都可以对上写自动化脚本也省事。我见过一些服务器上目录散落、进程名随便起三个月后再看根本不知道谁是谁。7.2 部署完成后的最后三步别省略每次部署接近尾声我都会按固定顺序做最后一次体检。第一步在服务器上执行 nginx -t确保配置没有语法问题第二步curl 外网域名下的接口地址确认从浏览器到 Nginx 到 Node 整条链路是通的第三步pm2 save 保存一次进程列表确保重启后进程还在。这三步做完我才敢跟项目组说上线完成。回头想想很多线上问题其实都出在省略了某一步要么改了 Nginx 没检查语法要么 Node 服务没加开机自启一重启服务器整个项目就人间蒸发。现在的我接到部署任务反而不太慌先把目录结构定下来再把 Nginx 转发配好最后才看业务代码。很多看似玄乎的线上故障梳理到最后都是没按层排查浏览器层、Nginx 层、Node 层一层一层 curl 过去问题总会现形。这套宝塔 Nginx Node 的部署套路我用了很久前后端分离带来的跨域焦虑也基本从我的日常里消失了。希望这些经验和踩过的坑能帮你少熬几个通宵。

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

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

免费获取报价