简介:这份文档面向需要将若依前后端分离项目落地上线的开发者与运维人员,围绕 Linux+Nginx 与 Windows+Tomcat 两套环境梳理部署路径,并补充调用第三方 API 时的跨域处理与部署后页面 404 的排查思路。文档还简要介绍若依的开源定位、Vue 与 Element UI 前端、Spring Boot 与 Spring Security 后端,以及 Redis、Jwt 权限认证和代码生成器等特性,帮助读者在动手前建立整体认识。压缩包共 1 个 docx 文件,约 223KB,内容按部署环境分块组织,对后台 jar 包启动、前端 dist 构建产物、Redis 缓存与 Nginx 代理之间的关系也有交代,便于按需查阅。已有 7766 人学习,说明其在同类部署资料中具有较高参考度。读者可从中获得分环境部署的步骤框架、Nginx 代理与 Tomcat 路径配置的关注点,以及跨域和 404 问题的定位方向,适合作为若依项目上线前的对照清单与排错参考。
1. 上线前一晚,我把若依前后端分离项目重新部了一遍
去年接手一个若依前后端分离项目,本地npm run dev配 IDEA 里跑的后端,接口通、菜单全、登录秒进,信心满满。打包扔到服务器之后,登录页能打开,输完账号密码跳转成功,然后——白屏。F5 一刷新,404。点左侧菜单,右边内容区一片空白。这三件事凑一起,够一个新手在机房待到凌晨三点。
问题一个都不在业务代码里,全在部署链路上:jar 怎么打、dist 放哪、nginx 的location怎么代、Tomcat 下service.xml和WEB-INF/web.xml怎么补、第三方地图 API 的跨域往哪一层甩。这份若依前后端分离部署文档把这些环节一条条列了出来,还额外给了 Windows + Tomcat 无 Nginx 的备用路线。刚接手若依要上线的人、二次开发完准备发版的熟手,都能直接照着抄。
2. 技术栈与目录结构:前后端分离到底分离了什么
若依这套框架的定位很明确——一个开箱即用的后台管理脚手架。前端 Vue + Element UI 负责渲染和交互,后端 Spring Boot + Spring Security + Redis + Jwt 负责认证、权限和数据。所谓「分离」,落到部署上是三个具体的物理事实:前端和后端是两次独立的构建、产生两份独立的产物、最后靠一个反向代理把两边的 URL 空间粘在一起。理解不了这一点,后面所有的 404 和白屏都无从解释。
2.1 前端 Vue 与后端 Spring Boot 的边界在哪
后端不渲染任何页面,只吐 JSON。所有请求统一走一个前缀常量,开发环境是/dev-api,生产环境是/prod-api,这个值放在前端根目录的.env.production里。前端 axios 的 baseURL 读的就是它,所以浏览器发出去的请求长这样:http://你的IP/prod-api/system/user/list。
而后端真正监听的路径其实没有/prod-api这一段,是/system/user/list。中间这段差额,就是反向代理要干的活——它是把/prod-api/前缀剥掉再转发,还是在转发时补齐前缀,取决于你proxy_pass结尾有没有斜杠,这个细节在第 3 章会专门讲。JWT 令牌放在请求头Authorization: Bearer xxx里,前端存 localStorage,后端由 Spring Security 的过滤器校验,校验通过后从 Redis 里取用户权限信息。这条链路里任何一环配错,表现都是「登录成功但什么都看不到」。
2.2 目录结构与三类构建产物
一个标准的若依前后端分离工程,模块划分大致如下:
| 目录 / 文件 | 作用 | 构建产物 |
|---|---|---|
ruoyi-ui/ | 前端工程,Vue + Element UI + vue-router + vuex | dist/目录 |
ruoyi-admin/ | 后端启动模块,含application.yml和主类 | target/*.jar或*.war |
ruoyi-framework/ | Security、JWT、Redis、拦截器、全局异常 | 打进入口包 |
ruoyi-system/ | 系统管理业务,用户、角色、菜单、字典 | 打进入口包 |
ruoyi-common/ | 工具类、常量、注解、通用返回体 | 打进入口包 |
ruoyi-quartz/ | 定时任务 | 打进入口包 |
ruoyi-generator/ | 代码生成器 | 打进入口包 |
多模块聚合工程用 Maven 管理,ruoyi-admin是唯一带spring-boot-maven-plugin的模块,打包时会把其余模块的 class 全部收进BOOT-INF/lib。所以你在target下看到的那一个几十兆的 jar,就是完整的可执行后端。
前端的产物只有一个dist目录,里面是index.html、static/js、static/css、static/fonts。路由是 history 模式,意味着/system/user这种地址是虚拟的,服务器上并不存在同名文件,也没有同名目录——这是后面所有 404 的根源。
2.3 运行时要件:Redis、Nginx、Tomcat 各自的分工
三个外部依赖各司其职,别搞混:
Redis 存两类东西,一是登录态相关的 token 缓存,二是字典、参数、菜单这些高频读数据的缓存。若依默认用 0 号库,application.yml里配spring.redis.host和port。Redis 没起或者密码不对,最典型的表现是登录接口报「认证失败」或者干脆连不上,日志里能看到RedisConnectionFailureException。
Nginx 在 Linux 方案里干两件事:把dist目录当静态站点托管,以及把/prod-api/反代到本机 8080 的 jar 上。两个 location 配好,一个 80 端口对外就够了。
Tomcat 只在 Windows 方案里出现,用来同时承载 war 包和 dist 静态文件。它承担了 Nginx 的角色,但配置方式完全不同——service.xml的Context和WEB-INF/web.xml都是坑点。
提示:先用
redis-cli ping确认返回 PONG,再动 nginx 配置。顺序反了,你会花半小时怀疑 nginx 写错了。
3. Linux + Nginx 部署:从 mvn clean package 到 nginx.conf 全链路
Linux + Nginx 是目前最主流的走法,也是打包产物最干净的一条路。整体节奏是「两打包、两上传、一代理」,中间不涉及容器改造。下面按执行顺序拆。
3.1 后端打包:mvn clean package 与 profile 的选择
在 IDEA 的 Terminal 里,或者直接在服务器上装好 Maven 后,进项目根目录执行:
# 跳过测试打包,避免测试用例连不上数据库导致构建失败 mvn clean package -Dmaven.test.skip=true # 打包完成后确认产物位置 ls -lh ruoyi-admin/target/*.jarclean清掉上一次的target,package触发编译 + 测试 + 打包。-Dmaven.test.skip=true是血泪经验:若依自带的测试类会去连数据源,本地能连、服务器上不一定能连,构建直接挂在那里,报的错还跟你真正关心的业务无关。
打包的 profile 由application.yml里的spring.profiles.active决定,也可以在启动时用--spring.profiles.active=prod覆盖。生产库连接、Redis 地址、文件上传路径这些都在application-prod.yml里。常见翻车是打了包才发现连的还是本地localhost:3306,服务器上根本没这个库。
生成的 jar 通常在ruoyi-admin/target/下,命名带版本号。用 scp 传上去:
scp ruoyi-admin/target/ruoyi-admin.jar root@你的服务器IP:/home/ruoyi/3.2 前端构建:npm run build:prod 与 publicPath
进ruoyi-ui目录,先确认 Node 版本别太新也别太旧,建议 16.x 或 18.x,Node 20 以上某些老版本 webpack 依赖会报ERR_OSSL_EVP_UNSUPPORTED。
# 安装依赖,国内可加 --registry 参数 npm install # 生产构建,--report 会额外生成 report.html 用于分析包体积 npm run build:prod --report构建完在ruoyi-ui/dist下拿到全部静态文件。--report这个参数不是必须的,但它生成的report.html能让你一眼看出哪个 chunk 特别大,后期优化首屏加载时有用。
关键配置在vue.config.js:
module.exports = { // 部署到根路径用 "/",部署到子路径用 "/子路径/" publicPath: process.env.NODE_ENV === 'production' ? '/' : '/', outputDir: 'dist', assetsDir: 'static', lintOnSave: false, productionSourceMap: false, devServer: { host: '0.0.0.0', port: 80, proxy: { [process.env.VUE_APP_BASE_API]: { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { ['^' + process.env.VUE_APP_BASE_API]: '' } } } } }publicPath决定了index.html里引用 js/css 的前缀。写成/时引用的是/static/js/app.xxx.js,配根域名访问没问题。如果你打算把前端挂在http://ip/ruoyi/这种子路径下,这里就要改成/ruoyi/,同时 nginx 的 location 和try_files都要跟着调。三者不一致,表现就是页面骨架出来了但样式全丢、JS 404。
3.3 nginx.conf:一个 server 块同时代理两端
把dist上传到/home/ruoyi/projects/ruoyi-ui,然后编辑/etc/nginx/nginx.conf,在http块里加一个 server:
server { listen 80; server_name localhost; # 前端静态资源,history 模式必须配 try_files location / { root /home/ruoyi/projects/ruoyi-ui; try_files $uri $uri/ /index.html; index index.html index.htm; } # 后端接口反向代理 location /prod-api/ { proxy_set_header Host $http_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 http://127.0.0.1:8080/; } }逐条解释。try_files $uri $uri/ /index.html是 history 路由的命门:请求/system/user时,服务器上并没有system/user这个文件,$uri和$uri/都失败,最后回落到index.html,交给前端路由去解析,页面才不会 404。
proxy_pass http://127.0.0.1:8080/;结尾那个斜杠决定了前缀是否被剥掉。带斜杠表示把/prod-api/替换成/,所以/prod-api/login转发到后端变成/login。不带斜杠则原样透传成/prod-api/login,后端没有这个映射,直接 404。这两个字符的差别,是我见过次数最多的「配置看着没问题但就是不通」。
Host和X-Forwarded-For这几个头是给后端拿真实 IP 用的,若依的登录日志模块会记录登录 IP,不传这些头,记下来的全是127.0.0.1。
改完配置先校验再重载:
nginx -t # 语法检查,必须看到 syntax is ok nginx -s reload # 平滑重载,不断开现有连接3.4 nohup 启动与日志落盘
jar 用前台方式跑,SSH 一断进程就没了。标准做法:
cd /home/ruoyi nohup java -jar ruoyi-admin.jar \ --spring.profiles.active=prod \ --server.port=8080 \ > msg.log 2>&1 &nohup让进程忽略挂断信号,> msg.log把标准输出重定向到文件,2>&1把标准错误也并进同一个文件,末尾的&放后台。msg.log生成在执行这条命令时所在的目录,不是 jar 所在目录,也不是target,这个细节坑过不少人——日志找不到时先pwd一下。
生产环境更稳妥的是把 JVM 参数也带上:
nohup java -Xms512m -Xmx1024m -jar ruoyi-admin.jar \ --spring.profiles.active=prod > msg.log 2>&1 &启动后别急着开浏览器,先看日志尾部有没有Started RuoYiApplication in x seconds,再curl http://127.0.0.1:8080/确认后端自己活着,最后才验证 nginx 这一层。分层验证能让你在三分钟内定位问题出在哪一层,而不是对着白屏瞎猜。
4. Windows + Tomcat 部署:war 包、service.xml 与 WEB-INF 兜底
有些内网环境只给一台 Windows 机器,装不了 Docker 也不想额外维护 Nginx,那就走 Tomcat 路线。这条路的核心变化是:后端从「独立进程 + 反向代理」变成「容器内应用 + 同容器静态资源」,Jar 要改 War,端口和上下文路径全部交给 Tomcat 管。
4.1 pom 改 war 并排除内置 Tomcat
打开ruoyi-admin/pom.xml,第一件事改打包类型:
<packaging>war</packaging>只改这一行,打出来的 war 里仍然会带内嵌 Tomcat 的 jar,扔进外部 Tomcat 会冲突报错。必须把 web starter 里的内嵌容器排掉:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>同时启动类要继承SpringBootServletInitializer并重写configure,否则外部容器找不到 Spring 的入口:
@SpringBootApplication public class RuoYiApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(RuoYiApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { // 外部 Tomcat 通过这个方法加载 Spring 上下文 return builder.sources(RuoYiApplication.class); } }重新mvn clean package,拿到ruoyi-admin.war。把它复制到 Tomcat 的webapps目录,并重命名为prod-api.war。这个名字不是随便起的——它要和前端请求前缀/prod-api对上,Tomcat 解压后的上下文路径就是/prod-api,请求/prod-api/login才能落到后端。
4.2 service.xml 的 Context 配置与白屏问题
前端dist里的内容整体复制到webapps下,假设放成webapps/dist。然后编辑conf/service.xml,做两处改动。
第一处,Connector 端口。若依默认后端跑 8080,Tomcat 默认也是 8080,如果两个都想留,就把 Tomcat 改成 80:
<Connector port="80" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />第二处,在<Host>节点内加 Context,把根路径指到 dist:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Context path="/" docBase="dist" reloadable="true" crossContext="true" /> </Host>path="/"表示根路径访问,docBase="dist"可以写相对webapps的路径,也可以写绝对路径。这一行不加会怎样?登录页能打开(因为/默认解析到了别的地方),登录成功后跳转/index,Tomcat 找不到对应资源,直接给一个空响应——这就是那个经典的「登录成功但白屏」。原因是前端本地跑的时候地址是ip:端口/#/路由或者ip:端口/路由,根路径没被正确指到index.html。
4.3 WEB-INF/web.xml 兜底刷新 404
Context 配好之后页面能进了,但用户在任意页面按 F5,又是一个 404。因为刷新请求的是/system/user这种虚拟路径,Tomcat 仍然按文件找。
解法是在dist目录下新建WEB-INF文件夹,里面放一个web.xml:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true"> <display-name>Router for Tomcat</display-name> <error-page> <error-code>404</error-code> <location>/index.html</location> </error-page> </web-app>这段配置的意思是:任何 404 都转发到index.html。作用等价于 Nginx 里的try_files ... /index.html,只不过换成了容器层面的错误页机制。
注意:
WEB-INF目录名大小写敏感,必须全大写;metadata-complete="true"不能省,省了 Tomcat 会去扫注解,启动变慢还可能有警告。
启动 Tomcat(bin/startup.bat),浏览器输入http://ip:80/,登录、刷新、点菜单这三步都过一遍,Windows 路线就算通了。
5. 部署后排查:404、白屏、菜单丢失与跨域的 5 条记录
部署这活儿,成功路径只有一条,失败路径能写出几十条。下面这 5 条是我在不同机器上真实撞过的,按「现象 → 原因 → 解决」记下来,遇到时按顺序对照。
5.1 刷新页面 404:history 模式没兜底
现象:首页能进,点进任意二级页面正常,按 F5 立刻 404,把地址栏里/system/user删成根路径又能进。
原因:Vue Router 用的是 history 模式,/system/user是浏览器地址栏的虚拟路径,磁盘上没有这个文件。Nginx 默认按静态文件找,找不到就 404;Tomcat 同理。
解决:Nginx 方案在location /里加try_files $uri $uri/ /index.html;;Tomcat 方案加 4.3 节那个WEB-INF/web.xml的 404 错误页映射。改完记得nginx -t && nginx -s reload,Tomcat 要重启才生效。
5.2 登录后白屏:代理前缀与上下文路径对不上
现象:登录接口返回 200,token 也存进去了,页面跳转后一片空白,控制台能看到/prod-api/getInfo请求 404。
原因:两种情况。Linux 下多半是proxy_pass少了结尾斜杠,/prod-api/getInfo被原样透传给后端,而后端只认/getInfo。Windows 下多半是 war 包没有重命名成prod-api.war,Tomcat 解压出的上下文是ruoyi-admin,实际路径变成/ruoyi-admin/getInfo。
解决:先看浏览器 Network 面板里那条失败请求的完整 URL,再对照后端实际的server.servlet.context-path(若依默认没有,就是空)。Nginx 侧确认proxy_pass http://127.0.0.1:8080/;带斜杠;Tomcat 侧确认webapps下解压出来的目录名和前端常量完全一致。
5.3 菜单打不开:vue.config.js 里那个被删掉的点
现象:第一次部署一切正常,改了几个菜单、换了几张图标重新打包上传,登录后侧边栏能出来,点任何菜单右侧内容区都是空白,控制台报 chunk 加载失败或者路由匹配不到。
原因:vue.config.js的publicPath被改成了./。相对路径在首页能正常解析,但一旦路由切到/system/user这种深层路径,相对路径的基准就变了,js 会去/system/static/js/xxx.js找,自然找不到。
解决:把publicPath改回'/',重新npm run build:prod再上传。判断方法很简单——打开dist/index.html,看里面 script 标签的 src 是以/static/开头还是./static/开头,后者就是元凶。
5.4 第三方 API 跨域:三层任选其一
现象:页面里接了第三方地图或天气接口,浏览器控制台报has been blocked by CORS policy,Network 里请求状态显示(failed)或者 200 但读不到响应。
原因:浏览器的同源策略。你的页面在http://你的IP,第三方接口在另一个域名,中间没有允许跨域的响应头,浏览器就把响应拦下了。这跟后端代码写没写对无关。
解决分三层,按部署形态选:
后端全局 CORS,适合第三方接口由自己控制的情况:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 注意:allowCredentials 为 true 时不能用 "*",要用 originPatterns .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }allowedOrigins("*")配allowCredentials(true)在 Spring 5.3 之后会直接抛异常,必须换成allowedOriginPatterns。这个方法只管你自己的后端接口,管不到前端直连的第三方域名。
开发环境代理,改vue.config.js的devServer.proxy,把第三方路径也加一条规则进去,让 dev server 代你请求。这只在npm run dev时有效,打包上线后失效,别指望它解决生产问题。
生产环境用 Nginx 反代,这是最通用的做法:
location /third-api/ { proxy_pass https://第三方接口域名/; proxy_set_header Host 第三方接口域名; proxy_ssl_server_name on; proxy_set_header X-Real-IP $remote_addr; }前端调用时把第三方 URL 换成/third-api/xxx,浏览器看来是同源请求,跨域问题从源头消失。proxy_ssl_server_name on在反代 https 且对方用了 SNI 时是必须的,不加会握手失败。
提示:三层不要同时上。后端开了全局 CORS,又在 Nginx 加了一层反代,响应头会重复,浏览器反而报
multiple values错误。
5.5 环境错配:Redis 没起、端口占用、profile 用错
现象:jar 启动日志刷了一屏红色,最后退出;或者启动成功但登录一直转圈。
原因:三种常见情况。Redis 没启动或密码不对,登录时取不到 token 缓存;8080 端口被上一次没杀干净的 java 进程占着,新进程Address already in use;spring.profiles.active还是dev,连的是不存在的本地数据库。
解决:按顺序查。redis-cli -h 你的IP ping返回 PONG 才算通;netstat -tunlp | grep 8080找到 PID 后kill -9;ps -ef | grep java看启动参数里带的 profile 对不对,不确定就直接在命令行用--spring.profiles.active=prod显式覆盖。
6. 二次开发后的重新部署:一套固定动作与验证清单
改了代码要重新上线,最忌讳的是「只传了改动的那个文件」。若依是多模块工程,改ruoyi-system里一个 Service,jar 必须整个重打;改前端一个页面,dist必须整个重建。下面这套动作我固定执行,顺序不能换。
# 1. 停掉旧进程,确认端口释放 ps -ef | grep ruoyi-admin.jar | grep -v grep | awk '{print $2}' | xargs -r kill -9 # 2. 后端重新打包(后端有改动时) mvn clean package -Dmaven.test.skip=true -pl ruoyi-admin -am # 3. 前端重新构建(前端有改动时) cd ruoyi-ui && npm run build:prod # 4. 上传并覆盖 scp ruoyi-admin/target/ruoyi-admin.jar root@host:/home/ruoyi/ rsync -av --delete ruoyi-ui/dist/ root@host:/home/ruoyi/projects/ruoyi-ui/ # 5. 启动并跟踪日志 ssh root@host "cd /home/ruoyi && nohup java -jar ruoyi-admin.jar --spring.profiles.active=prod > msg.log 2>&1 &" ssh root@host "tail -f /home/ruoyi/msg.log"-pl ruoyi-admin -am是只构建入口模块及其依赖,比全量打包快一截。第 4 步的rsync --delete很关键,它会把服务器上旧版本残留的 chunk 文件删掉——不清的话,老用户浏览器缓存里指向旧文件名,加上新文件同名不同内容,就会出现加载到一半报错的情况。
菜单相关的改动有个特殊注意点:若依的菜单是存在数据库sys_menu表里的,前端页面改动要配合菜单管理里的路由地址、组件路径一起调。组件路径写system/user/index对应src/views/system/user/index.vue,少一层目录、多一个后缀都会导致菜单点开后空白,而浏览器控制台只有一条不痛不痒的警告。改完菜单后建议清一次 Redis 里的字典和菜单缓存,或者直接重启后端让缓存重建。
用代码生成器一键生成的新模块,部署前还要确认三件事:sys_menu里有没有生成对应的菜单记录、ruoyi-admin的application.yml里 mapper 扫描路径有没有覆盖新包、新表如果在别的库,多数据源配置有没有补。这三件事漏一件,症状都是「代码部署上去了,菜单也点了,页面 404 或者接口 500」。
验证按四层走:curl http://127.0.0.1:8080/确认后端活着;curl -I http://127.0.0.1/确认 nginx 返回 200;浏览器登录后点三个不同层级的菜单;最后在任意二级页面按 F5 确认不 404。四步全过,再通知业务方验收。
这几年我最大的习惯变化,是不再相信「本地能跑就行」。每次发包前,必在一台干净的测试机上按上面这套流程完整走一遍,包括 kill 进程、重新打包、rsync 覆盖、看日志启动完成,一步不省。翻车过太多次之后你就会明白,部署这件事没有玄学,只有漏掉的步骤。希望帮到你。
本文还有配套的精品资源,点击获取