Nginx反向代理解决跨域+部署SpringBoot前后端分离项目实战
前后端分离的项目做多了尤其是刚从前端开发转全栈或者一个人包揽前后端的时候最烦的其实就是两件事一是开发环境里的跨域问题二就是生产环境要把前端打包文件和一个SpringBoot的jar包塞到同一台服务器上让它们像一家人一样和睦共处。标题里这条Nginx反向代理解决跨域问题访问后台接口同一个服务器部署springboot项目前后端分离我一看就知道是很多同学卡了很久的典型场景。先说个结论只要你的前后端最终部署在同一台服务器上用Nginx做反向代理就是最省事、最稳定、也最能兼顾以后扩展的方案。它不只是把跨域这个症状压下去了而是直接从架构上把跨域这个概念干掉了——让浏览器访问的是同一个域名、同一个端口自然就不存在跨域了。本文会把跨域的底层逻辑、Nginx配置的完整写法、SpringBoot项目从打包到上线全流程、以及我在实际部署中踩过的坑一次说清适合正在做前后端分离项目、打算独立上线部署的开发者和运维新手参考。1. 跨域到底是怎么来的别急着配Nginx先把这事想明白1.1 浏览器同源策略不信任任何别人家的接口很多新手一遇到跨域第一反应就是前端Nginx配一下就行但如果搞不清同源策略后面出了问题会非常抓瞎。所谓同源指协议、域名、端口三者完全一致。你的前端页面跑在http://example.com:8080后端接口跑在http://example.com:9090/api端口不同就算域名是同一个浏览器也会认为是不同源于是默认发起安全检查。这个检查不是Nginx做的也不是SpringBoot做的而是浏览器自己干的。浏览器加载静态页面之后如果JavaScript发了一个XMLHttpRequest或者fetch请求它会检查这个请求的目标地址是否与当前页面同源。不同源的话常规的GET请求会被发出去但响应会被浏览器拦截你根本拿不到数据非简单的POST、PUT、DELETE请求浏览器还会先发一个OPTIONS预检请求问服务器我可以这样请求吗服务器不配合就会直接失败。我把这个机制理解成小区保安你必须住在这个小区里的人才能进门访客要进必须先去物业登记预检请求。物业不配合保安访客就被堵在门外。很多前端说请求发出去了浏览器F12里也看到了数据但就是报错原因就在这里——请求确实到了服务器服务器也返回了数据但浏览器因为安全机制不让你读。这不是接口坏了是浏览器不让。1.2 前后端分离后跨域是必然结果而不是意外传统单体项目里JSP或者Thymeleaf页面和后端接口是同个服务提供的页面和接口天然同源不会跨域。但前后端分离之后前端项目用Vue、React等框架开发开发时跑在webpack-dev-server的本地端口比如localhost:8080后端SpringBoot跑在localhost:8081这俩一碰就是典型的跨域场景。生产环境同样如此很多人会把前端打包后的dist目录扔到Nginx里把SpringBoot打成jar包跑在其他端口比如后端跑8080前端静态资源也放在Nginx的默认80端口下。这样虽然在一个服务器上但浏览器看到的是http://域名:80的页面去请求http://域名:8080/api的接口端口不一样还是跨域。所以同一个服务器这个条件并不能自动消除跨域问题必须由中间层代理去处理。这也是为什么Nginx反向代理方案能一劳永逸——它不是像SpringBoot的CORS配置那样去允许跨域而是直接把后端接口搬到和前端相同的服务路径下让浏览器从根本上不产生跨域行为。1.3 CORS配置和Nginx反代到底该怎么选这里有个很容易被忽略的选择题。后端可以加CrossOrigin或者全局CORS配置这个办法开发联调时很好用但生产环境如果继续用你会发现安全问题很难收敛它把所有来自某个Origin的请求都放行了等于保安看到同一个小区的人就直接放进去完全不看你要去的楼层和目的。而且一旦后面要加新的前端域名你又要改后端配置、重新打包、重新发布维护成本高。Nginx反向代理则是把这个不同源问题消解在门口。我常用的方案是后端只监听本机端口或者即使监听在公网地址也不直接暴露给浏览器所有客户端的请求都先打到Nginx由Nginx根据路径前缀把/api的请求转给后端。这样后端根本不需要关心CORS跨域在源头就不存在了。对于生产环境来讲我明显更推荐后者。2. Nginx反向代理解决跨域的核心思路2.1 同域化代理让前端看起来只有一台服务器Nginx反向代理的原理很简单但很多人只是照抄配置不理解为什么这样就能解决跨域。我举一个最直白的例子浏览器向http://你的域名/a/header发起请求Nginx看到路径前缀是/a就把请求转发到http://127.0.0.1:8081/header也就是后端真实的接口路径。整个请求过程中浏览器只知道你请求的是http://你的域名/...它根本不知道后端8081端口的存在更不会觉得这个请求来自别的源。关键操作就是location块里的proxy_pass指令。这个指令决定了Nginx用什么方式转发请求。配置得当的话前端所有请求都走相对路径前后端在一个源下跨域问题烟消云散。这里的核心不是如何允许跨域而是如何伪装成同域思想完全不同。下面是一个我实际用过的精简配置骨架先给你感受下结构server { listen 80; server_name your_domain.com; # 前端静态资源 location / { root /usr/share/nginx/html; # 前端打包后的dist目录 index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8081/; 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; } }乍一看很简单但中间有几个细节直接决定了能不能跑通我挨个讲。2.2 proxy_pass的斜杠陷阱多一个少一个请求路径就变了这是我在指导别人配置时发现最容易翻车的点。proxy_pass http://127.0.0.1:8081;和proxy_pass http://127.0.0.1:8081/;看起来只差一个斜杠但转发逻辑完全不同。不带斜杠location /api/匹配到请求/api/user/list转发到后端时完整路径是http://127.0.0.1:8081/api/user/list即Nginx不会去掉匹配的前缀原样转发。带斜杠同样请求/api/user/list转发到后端的路径是http://127.0.0.1:8081/user/list即Nginx会去掉/api/这部分前缀然后再把剩余部分拼到代理地址上。所以你需要根据SpringBoot后端的真实接口路径来决定。如果后端Controller的映射是RequestMapping(/api/user)前端请求路径是/api/user/list那么你既可以用不带斜杠的方式把整个/api路径原样转发也可以用带斜杠的方式在后端接口里不写/api前缀让Nginx帮你去掉。我个人的习惯是后端接口统一带/api前缀Nginx用不带斜杠的写法这样后端接口语义清楚Nginx配置也一目了然后续做网关、做权限拦截时都更直观。2.3 静态资源与接口共存时的location匹配优先级同服务器上既要托管前端dist目录又要代理后端接口就必须理解Nginx的location匹配规则。location /和location /api/放在同一个server里并不冲突因为Nginx会先看请求路径是否命中更精确的/api/前缀如果命中就进入代理规则否则落入/的静态资源规则。但如果你把静态资源和接口的路径搞混了比如前端dist里也有一个api目录就会出现请求被静态资源规则拦截返回404或者返回一个index.html而不是接口数据。这个问题特别隐蔽我见过有人排查了老半天最后发现是因为dist目录里残留了旧的api文件。另外如果你的前端应用是Vue或React开发的单页应用路由是前端路由比如/home、/about在刷新页面时如果Nginx只配了location / { root ...; }没有try_files那么刷新/home的时候Nginx会在dist目录里找home文件找不到就返回404。解决方式是加一行try_files $uri $uri/ /index.html;意思是如果找不到对应文件就回退到index.html让前端路由接管。这一点在部署单页应用时是标配不能省。3. SpringBoot前后端分离项目完整部署实操3.1 部署前的整体规划目录、端口、权限一次定好动手部署前先把服务器上的端口、目录、服务结构规划好别想到一步做一步。我习惯这样安排前端静态文件放在/usr/share/nginx/htmlNginx默认已经能读取不用额外加权限。SpringBoot的jar包放在/opt/springboot/app.jar日志输出到/opt/springboot/logs。Nginx监听80端口SpringBoot监听内网地址127.0.0.1:8081不直接对外暴露。为什么后端要监听在127.0.0.1而不是0.0.0.0因为所有请求都走Nginx转发后端服务对公网是透明的。如果后端监听在0.0.0.0:8081相当于公网也能直连后端接口绕过Nginx这样既失去了统一的日志入口也增加了不必要的暴露风险。生产环境务必让后端只监听内网端口。SpringBoot配置文件里这样写server.address127.0.0.1 server.port8081如果是application.yml就是server: address: 127.0.0.1 port: 8081设置完之后可以本地验证一下curl请求http://127.0.0.1:8081/actuator/health如果通了说明服务本身没问题。3.2 前端构建dist目录的生成与上传细节前端项目在本地执行构建命令Vue项目通常是npm run buildReact项目是npm run build或yarn build。构建会在项目下生成一个dist目录。这个目录里面通常是index.html、static/js、static/css等静态资源。但这里有一个极其关键的注意点如果你使用的还是老式的Vue CLI项目比如Vue 2vue.config.js里的publicPath默认是/如果你的Nginx不是把前端放在域名根路径下而是放在类似http://域名/myweb/子路径下那么构建时就必须设置publicPath: /myweb/否则资源路径全乱套。如果使用Vite对应配置项是base。对于部署在根路径下的情况默认配置就行。构建完成后先把dist目录在本地压缩成zip上传到服务器后再解压。为什么建议先压缩再上传因为dist里可能有几百上千个碎文件逐个上传遇到网络差的时候会非常痛苦而且容易漏文件。我一般用scp或者sftp上传zip包然后解压到目标目录unzip -o dist.zip -d /usr/share/nginx/html/注意先清空目标目录里之前的旧文件避免残留旧资源导致各种奇怪问题rm -rf /usr/share/nginx/html/*3.3 SpringBoot项目打包上传jar包的构建与启动方式SpringBoot后端打包我建议在开发机上用Maven命令构建mvn clean package -DskipTests如果项目使用gradle则是gradle clean bootJar打包之后target目录下会生成xxx.jar。通常我喜欢把jar包重命名成简单点的名字比如app.jar然后把整个构建文件比如只保留jar包上传到服务器的/opt/springboot目录。启动SpringBoot的方式很多但上线部署不建议直接用java命令跑前台进程因为一旦你关掉SSH窗口进程就挂了。可以用nohup把进程放后台nohup java -jar /opt/springboot/app.jar --spring.profiles.activeprod /opt/springboot/logs/app.log 21 解释一下--spring.profiles.activeprod用于指定生产环境配置日志输出到app.log21把错误输出也重定向到同一个文件里。启动之后别急着走先看几眼日志tail -f /opt/springboot/logs/app.log确认没有异常端口也能正常访问后再继续。如果你有systemd使用经验也可以把SpringBoot注册成系统服务这样开机自启和崩溃重启都省事但如果是小项目nohup启动也没毛病。3.4 完整的Nginx配置实战一个server块搞定所有问题现在把前面提到的要点整合在一起给出一份可以复制修改的完整配置。假设你的前端就是Vue单页应用后端接口都以/api开头服务器是CentOS/Ubuntu系Nginx用默认用户运行。server { listen 80; server_name your_domain.com; access_log /var/log/nginx/your_domain.access.log; error_log /var/log/nginx/your_domain.error.log; gzip on; gzip_min_length 1k; gzip_types text/plain text/css application/javascript application/json image/svgxml; gzip_vary on; # 接口代理 location /api/ { proxy_pass http://127.0.0.1:8081; # 不带斜杠保留 /api 前缀 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; } # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp)$ { expires 30d; add_header Cache-Control public, no-transform; } }这里有三个细节要重点说。第一proxy_set_header Host $host;这行很重要。它让后端收到请求时的Host头是浏览器访问的域名而不是127.0.0.1:8081。如果你的后端代码里做了一些基于请求Host的逻辑比如生成跳转URL、判断域名这行就不写不行。第二proxy_read_timeout和proxy_connect_timeout要根据业务来调。如果后端有导出Excel、生成报表之类的耗时长接口默认的60秒可能不够建议调成300秒或者更长。但也要注意这个超时时间不能无脑放大否则恶意请求可以占用线程很久。第三gzip配置不是必须的但对前端体验提升很明显。尤其是application/json接口响应开启gzip后体积可以减少70%以上对移动端网络省流量非常有帮助。配置写好后先做一次Nginx语法检查nginx -t出现syntax is ok就表示没问题然后重新加载配置即可nginx -s reload3.5 测试验证的完整路径从静态页面到接口全链路配置完成后不能只看页面能打开就完事要用一组完整步骤来验证。第一步浏览器直接访问http://your_domain.com/看是否能打开前端首页。 第二步打开浏览器F12的Network面板刷新页面看前端资源js、css是否正常加载状态码是否是200。如果有404基本就是静态资源路径问题。 第三步在Network里找一个前端发起的API请求看它的请求URL是什么状态码是什么。如果请求URL是/api/user/info且返回了JSON数据说明代理生效。如果请求URL是http://your_domain.com:8081/api/...说明前端axios/fetch配置的baseURL写死了端口跨域问题依然存在你需要改前端配置。很多项目里前端axios默认baseURL写的是/api这个最好。但有些从本地开发环境复制过来的代码baseURL会写成http://localhost:8081/api或者http://192.168.1.100:8081/api这种写死在生产环境就是隐藏炸弹。部署后一定要全局搜索代码里的IP和端口改成相对路径// axios 配置 const service axios.create({ baseURL: /api, timeout: 10000 });改完前端代码后需要重新构建、上传dist这是最容易被忽略的一步。4. 实战中常见的坑和排查技巧4.1 接口404location路径和proxy_pass是否匹配如果你配置完之后页面能打开但所有API请求都返回404大概率是路径前缀处理出了问题。我举两个真实场景。场景一后端Controller映射是RequestMapping(/user/list)前端请求了/api/user/listNginx配置location /api/ { proxy_pass http://127.0.0.1:8081; }。因为不带斜杠转发到后端时会保留/api前缀所以后端收到的是/api/user/list但后端没有这个映射于是404。解决方法是后端Controller加上/api前缀或者Nginx的proxy_pass改成带斜杠。场景二后端接口很规范全部带/api但Nginx的proxy_pass带了斜杠例如proxy_pass http://127.0.0.1:8081/;。这时Nginx把/api前缀去掉再转发后端收到的是/user/list同样是404因为后端接口不是这个路径。排查方法就是在服务器上用curl模拟请求curl -v http://127.0.0.1:8081/api/user/list如果Nginx转发有问题你再curl一下域名对应的代理地址curl -v http://127.0.0.1/api/user/list -H Host: your_domain.com对比两次返回内容能直接定位问题出在后端还是Nginx。4.2 接口502 Bad GatewaySpringBoot没起来或防火墙拦截502是Nginx常见错误码意思是Nginx把请求转发到上游服务器但上游服务器没有正确响应。大多数情况是SpringBoot进程没起来或者监听的端口不对。先看Nginx错误日志tail -n 100 /var/log/nginx/error.log如果日志里有connect() failed (111: Connection refused)说明SpringBoot根本没在127.0.0.1:8081上监听。检查一下Java进程ps -ef | grep java如果进程存在但又连不上就用ss -lntp看端口监听状态ss -lntp | grep 8081假如你发现SpringBoot监听的是0.0.0.0:8081而Nginx连的是127.0.0.1:8081两者会有细微差别有时你的服务器上其他服务占用了127.0.0.1的IP可能也会有影响不过更多时候监听0.0.0.0是能连通的。最稳妥的做法是明确设置server.address127.0.0.1。另外如果你在云服务商控制台设置了防火墙规则仅放行了80端口而SpringBoot监听在0.0.0.0:8081那么浏览器直连http://公网IP:8081会被防火墙拦截但Nginx转发到内网127.0.0.1:8081是走本机回环不受云防火墙限制。这种情况下502不会出现但公网直连也确实是堵住了安全效果刚好。4.3 刷新页面就404单页应用路由需要try_files兜底这个坑基本每个Vue/React部署者都会遇到一次。前端首页http://域名/能打开但是浏览器直接访问http://域名/login或者http://域名/detail/1就返回404。原因就是Nginx在location /里没有配置try_files成/index.html。生产环境里用户经常通过链接分享直接访问某个深层路由或者刷新当前页面如果你不做try_files兜底Nginx会在root目录下找login这个文件或者文件夹找不到就404。加一行try_files $uri $uri/ /index.html;之后当uri对应文件不存在时自动重写到index.html浏览器加载到前端应用后再由前端路由解析出真正的页面。这是单页应用部署的必备操作切勿省略。4.4 跨域问题依然存在检查前端请求URL和注意OPTIONS预检有同学按照上面配置配完F5刷新页面居然还报跨域。这时候第一时间看Network里请求的URL到底是什么。如果浏览器发出的请求就是http://your_domain.com:8081/api/...说明你前端代码里的axios baseURL写死了8081。即便Nginx配置了代理前端根本就没走Nginx而是走了公网后端端口。这种情况你改后端CORS是不太合适的正确做法是修改前端baseURL重新构建让所有请求都走同源相对路径。另一种情况是请求走了Nginx但仍然报CORS错误。这往往是因为后端在GlobalCorsConfig里放行了某些跨域规则但Nginx没有透传OPTIONS请求或者后端拒绝处理OPTIONS。如果你采用了Nginx反代方案我建议在后端直接去掉CORS配置让后端只管业务跨域问题全部交给Nginx解决。如果后端代码确实写死了CORS而你暂时来不及改可以在Nginx层拦截OPTIONS请求直接返回200location /api/ { if ($request_method OPTIONS) { return 204; } proxy_pass http://127.0.0.1:8081; }但这个方法只适合过渡期因为直接拦截OPTIONS后端就完全感知不到预检请求了。如果业务里有依赖OPTIONS逻辑的场景会引入新问题。所以我建议最终还是要统一到后端不配CORS、Nginx统一代理的方案上。4.5 常见问题速查表现象大概率原因排查手段解决方式首页打开API全部404proxy_pass斜杠和后端路径不匹配curl带Host访问Nginx统一后端接口前缀/修正proxy_pass斜杠API请求502SpringBoot进程挂掉或端口未监听查看error.log、ss -lntp启动服务修正监听地址深层路由刷新404try_files未配置直接访问内页路由观察增加try_files兜底页面能开但API的URL仍带8081端口前端baseURL写死端口浏览器Network查看请求地址修改前端为相对路径并重新构建Nginx配置保存后不生效忘记reload或语法错误nginx -t reload修正配置并执行nginx -s reload上传图片/文件接口超时默认proxy_read_timeout不足观察日志和浏览器耗时调整proxy_read_timeout5. 一些值得关注的扩展与优化5.1 同一服务器上多项目共存的Nginx配置思路如果你后续要在同一台服务器上部署第二个、第三个前后端分离项目Nginx配置不应该无限堆进同一个server块而是该按项目域名或端口区分server。我用过的方式是让每个项目拥有独立的域名或二级域名对应的server块里各自处理各自的静态资源和接口。这样不同项目之间的环境完全隔离互不干扰。如果只有一个域名但想部署多个项目可以通过不同的子路径区分比如http://域名/app1/和http://域名/app2/。这种情况下前端构建时就要把publicPath/base分别设置为/app1/和/app2/Nginx里也要针对子路径单独配置session和cookie的Path也需要相应调整复杂度会高一些。假如不是强需求我更建议用不同端口或子域名来做多项目隔离省心得多。5.2 带上HTTPS后Nginx反向代理的额外配置现在全网都在普及HTTPS你也迟早要面对升级。Nginx配置HTTPS后反向代理部分需要额外注意一个问题后端是HTTP监听但浏览器和Nginx之间是HTTPS后端收到请求后如果生成了跳转地址可能还是http开头导致页面跳转出现不安全警告或用户被导来导去。在我的项目里我会在Nginx里加上proxy_set_header X-Forwarded-Proto $scheme;并且在SpringBoot配置文件里加上server.forward-headers-strategyframework这样后端在生成重定向URL或者读取协议相关信息时就可以识别出原始请求是HTTPS保持协议一致。如果你用的是云服务商的负载均衡或者CDN同理也要把X-Forwarded-Proto传递下去避免类似问题。5.3 日志与监控上线后不要只看浏览器上生产了不是万事大吉我强烈建议把Nginx和后端的日志至少保留30天以上。Nginx的access_log里记录了所有请求路径、状态码、响应时间、来源IP排查用户反馈问题的时候第一件事就是从日志复现请求链路。SpringBoot的日志也要配置好滚动策略避免单个日志文件越来越大占满磁盘。我的习惯是给access_log加上$request_time或$upstream_response_time这样能直接看到哪些接口响应慢log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for rt$request_time uut$upstream_response_time; access_log /var/log/nginx/access.log main;加上之后用命令行排个序找出响应最慢的几个接口再单独优化比靠用户感觉强得多。5.4 部署时一个不起眼但很关键的小习惯我用nohup方式启动SpringBoot时经常会遇到服务器重启后进程丢失忘了重新启动导致线上服务中断半天没发现。后来我的习惯是每次部署完都顺手写一个简单的重启脚本或者systemd服务单元把启动、停止、查看状态这些操作固化成命令。一个简化版的systemd单元文件大概是这样[Unit] DescriptionSpringBoot App Afternetwork.target [Service] WorkingDirectory/opt/springboot ExecStart/usr/bin/java -jar /opt/springboot/app.jar --spring.profiles.activeprod ExecStop/bin/kill -s TERM $MAINPID Restartalways Userwww-data [Install] WantedBymulti-user.target配置好之后systemctl start app、systemctl enable app开机自启和崩溃自动重启都解决了比我自己手动nohup靠谱得多。如果你在机器上装了多份JDK尤其注意ExecStart里的java路径是不是正确。回到最初的问题Nginx反向代理解决跨域、部署SpringBoot前后端分离项目其实是一个一次性搞定长期受益的架构优化。把跨域问题解决在Nginx层后端代码更干净把前后端部署在一台服务器上用同一域名、同一端口对外提供服务后续扩展HTTPS、负载均衡也都能顺理成章接上。我在实际部署中最大的感受是这个方案的问题通常不是配置本身有多难而是前端、Nginx、后端三者之间的路径约定不够统一导致一轮一轮踩坑。只要在项目开始时就把接口前缀、请求路径、部署目录三方约定清楚后面整个流程顺畅得令人意外。希望这篇文能帮正在折腾这个问题的同学少走几步我已经替你们走过的弯路。