ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RuoYi-Vue Pro权限解耦与多租户实战:Spring Boot+MyBatis Plus架构解析

RuoYi-Vue Pro权限解耦与多租户实战:Spring Boot+MyBatis Plus架构解析 简介这是一份基于Spring Boot、MyBatis Plus与Vue技术栈实现的RuoYi-Vue Pro后台管理系统设计源码面向需要快速搭建企业级中后台应用的开发者、架构师与研发团队。系统在经典RuoYi框架基础上集成了微信小程序覆盖RBAC动态权限、数据权限、SaaS多租户、Flowable工作流、第三方登录、支付短信以及商城、CRM、ERP、AI等业务模块几乎囊括企业后台常见能力可作为权限中台、业务基座或二次开发蓝本。压缩包共1193个文件体积20.74MB其中Java源码多达910个清晰呈现后端业务分层JPG与PNG图片124个用于前端界面与文档配图VM模板、XML配置、MD说明、HTTP接口示例和SQL脚本等120余个分别服务于页面渲染、系统配置、使用文档、接口调试与数据库初始化目录结构完整且便于检索。目前已有736人学习下载。通过这份资源读者既能研究主流框架整合、安全认证、多租户隔离与工作流集成等设计思路也能利用附带的前后端工程、初始化脚本与部署配置直接支撑企业级后台项目的快速落地与二次开发。1. 先别急着npm install这套源码的“权限解耦”才是拆包重点我拆这份源码时第一件事不是启动前端而是把 ruoyi-admin 下的 Application 跑起来顺着 915 个 Java 文件理清了模块边界。RuoYi-Vue Pro 本质上是把若依前后端分离版做成了企业级脚手架后端是 Spring Boot MyBatis Plus前端是 Vue Element但真正的复杂度不在 CRUD而在动态权限、数据权限和 SaaS 多租户的叠加。它适合两类读者一类是只需要快速交付后台管理系统想拿它做基线再开发另一类是想搞明白 RBAC 动态菜单、数据权限拦截器、Flowable 工作流在同一个事务边界里是怎么共存的。接下来按“启动、权限、租户、部署”这条线把这套架子拆开看。2. 工程结构与启动路径Spring Boot MyBatis Plus Vue 三端的依赖关系2.1 从目录结构判断模块边界拿到源码后先看根目录RuoYi-Vue Pro 采用多模块 Maven 工程前端和后端完全分离。后端按业务边界拆成ruoyi-admin、ruoyi-framework、ruoyi-system、ruoyi-modules其中 modules 下面又挂 job、generator、workflow、mall 等可选模块。这样做的好处是工作流、商城、CRM 这类强业务模块可以按需在ruoyi-admin/pom.xml中添加依赖而不是一开始就砸进同一个可执行包里。ruoyi-ui/ # Vue 3 Element Plus 前端工程 ruoyi-admin/ # 启动模块集中维护数据源、Security 过滤链 ruoyi-framework/ # 公共层Redis、异常处理、通用工具 ruoyi-system/ # 系统业务用户、角色、菜单、部门、字典 ruoyi-modules/ ├── ruoyi-job/ # 定时任务 ├── ruoyi-generator/ # 代码生成器 ├── ruoyi-workflow/ # Flowable 工作流 ├── ruoyi-mall/ # 商城模块可选依赖 └── ruoyi-crm/ # CRM 模块按需启用这种结构的直接好处是编译期就能暴露模块污染风险不会出现“万能 service”里互相引用的情形。ruoyi-admin是所有业务模块的最后聚合点可执行的 jar 从这里打出来开发时改了某个模块只需要mvn install -DskipTests后再启动 admin 进程不用把无关模块全部重新 install。遇到过不少人不把 Maven 根项目导入 IDEA而是单独打开 ruoyi-system导致编译时找不到 framework 中的公共类。这是最常见的错误姿势。正确做法是把整个 ruoyi 目录作为 Maven 工程导入IDEA 会把ruoyi-framework、ruoyi-system当成依赖模块调试时可以直接跳进 jar 包对应的源码。2.2 初始化数据库与多环境配置打开ruoyi-admin/src/main/resources/application-druid.yml可以看到支持主从动态数据源。默认情况下使用master数据源业务分库时可以继续扩展slave。下面是最小可启动配置spring: datasource: dynamic: primary: master datasource: master: url: jdbc:mysql://localhost:3306/ry-vue-pro?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379初始化时先创建空的ry-vue-pro库再执行sql/ry-vue-pro.sql结构脚本如果启用了工作流需要把sql/flowable.sql一并导入。这里的关键参数是serverTimezoneMySQL 8 驱动要求显式设置时区否则 Date 类型字段在查询时会出现 8 小时偏移。Redis 是必须的验证码、令牌、在线用户都会放进 Redis没启动 Redis 时登录接口会立刻抛连接超时。配置项实际作用常见错误spring.datasource.dynamic.primary默认数据源名称写成 driver-class-name 导致切换失败driver-class-nameMySQL 驱动类MySQL 5 用com.mysql.jdbc.Driver8 用com.mysql.cj.jdbc.DriveruseSSLfalse禁止 SSL 握手本机连接不关闭 SSL 会频繁告警serverTimezoneAsia/Shanghai时区校正不配置会报 CST 无法识别动态数据源是com.baomidou.dynamic.datasource提供的能力MyBatis Plus 框架内置了与它配合的插件。primary: master的含义是默认走 master 数据源如果启动时把主库的密码写错日志里会看到dynamic-datasource抛出的CannotGetJdbcConnectionException此时先检查密码和权限不是改连接池参数能解决的。2.3 用 IDEA 跑通前后端分离开发环境后端直接用 IDEA 打开 Maven 根工程等依赖解析完毕后运行ruoyi-admin/RuoYiApplication.java。如果本地没有配置 Maven 私服需要先在根目录执行mvn install -DskipTests把 framework 和 system 模块装进本地仓库。mvn install -DskipTests -Dmaven.javadoc.skiptrue cd ruoyi-admin mvn spring-boot:run -Dspring-boot.run.profilesdev-DskipTests跳过测试用例-Dmaven.javadoc.skiptrue跳过 javadoc 插件能省不少编译时间。spring-boot.run.profilesdev指定启动环境对应加载application-dev.yml。这里要注意IDEA 里直接运行 RuoYiApplication 时Environment 默认不是 dev需要手动在 Run Configuration 的 Program arguments 中加--spring.profiles.activedev否则某些环境变量可能缺失。前端在ruoyi-ui目录下操作先安装依赖再启动开发服务器cd ruoyi-ui npm install npm run dev前端开发服务器默认监听http://localhost80 端口页面里的请求由/dev-api代理到后端 8080。如果浏览器打开后接口 404检查vue.config.js中的proxytarget 是否为http://localhost:8080。这一整套流程跑通后登录页能出现验证码图片说明 Redis、数据库、前端代理三者都正常了。提示若依赖下载速度慢把 npm 镜像切到国内 registry 或使用 pnpm依赖锁定文件是package-lock.json切换包管理器前先删除旧的 lock 文件。3. 动态权限与数据权限MyBatis Plus 拦截器如何改写 SQL3.1 RBAC 与表结构设计RuoYi-Vue Pro 的权限模型是经典 RBAC用户关联角色角色关联菜单菜单上挂权限标识符。这套模型在内部系统很稳核心表拆成四张主表加若干关联表sys_user、sys_role、sys_menu、sys_dept其中部门表是数据权限的天然边界。表名关键字段设计意图sys_useruser_id, dept_id, user_name用户只挂部门和用户名校验sys_rolerole_id, role_key, data_scopedata_scope 控制数据可见范围sys_menumenu_id, perms, visibleperms 是前端按钮的权限字符串sys_deptdept_id, ancestorsancestors 存放父子路径方便递归把data_scope放在角色表而不是用户表是设计上很聪明的地方权限绑定到角色意味着同一用户在不同部门可以复用一套数据规则如果放在用户表每次调整数据范围都要改用户行审计和批量变更都麻烦。常见的 data_scope 值有 1全部、2本部门、3本部门及以下、4仅本人和 5自定义部门。从数据库视角获取用户权限最常见的一条 SQL 是SELECT DISTINCT m.perms FROM sys_menu m INNER JOIN sys_role_menu rm ON rm.menu_id m.menu_id INNER JOIN sys_user_role ur ON ur.role_id rm.role_id WHERE ur.user_id #{userId} AND m.perms IS NOT NULL AND m.perms ! ;这条查询结果直接交给 Spring Security在前端请求PreAuthorize(ss.hasPermi(system:user:list))这类注解时做匹配。细化点在于perms可以是逗号拼接的多个权限拿到结果后需要split(,)拆成集合否则hasPermi(system:user:add)会因找不到精确匹配而拒绝访问。3.2 数据权限注解与 AOP 拼接动态权限解决的是“你能看到哪些菜单”数据权限解决的是“你能看到哪些行”。RuoYi 的实现思路是定义DataScope注解在 Mapper 方法或 Service 方法上声明部门别名和用户别名再用 AOP 切进业务方法拼接 SQL 片段。Aspect Component public class DataScopeAspect { Before(annotation(dataScope)) public void doBefore(JoinPoint point, DataScope dataScope) { // 从安全上下文获取当前登录用户 LoginUser loginUser SecurityUtils.getLoginUser(); SysUser user loginUser.getUser(); // 根据角色 data_scope 生成条件片段 String sqlFilter buildDataScopeSql(user, dataScope.deptAlias(), dataScope.userAlias()); // 把片段塞进 BaseEntity.paramsXML 中通过 ${params.dataScope} 引用 Object arg point.getArgs()[0]; if (arg instanceof BaseEntity base) { base.getParams().put(dataScope, sqlFilter); } } }deptAlias()和userAlias()分别指定 SQL 中部门表和用户表的别名比如在 XML 里写了d和u注解上就写deptAlias d, userAlias u。buildDataScopeSql内部会根据 data_scope 分支拼出d.dept_id in (...)或u.user_id 1这样的片段XML 结尾统一写if testparams.dataScope ! null and params.dataScope ! AND ${params.dataScope}/if。这里有一个容易被忽略的安全点${params.dataScope}是字符串替换不是预编译的#{}。如果 AOP 拼接的内容混入用户输入会产生注入。RuoYi 的防线是只根据角色 data_scope 类型内部计算不直接消费前端传入的字符串二次开发自定义数据规则时也要走白名单映射。对比 MyBatis Plus 自带的DataPermissionInterceptorRuoYi 的注解方案更贴近业务可以在同一个方法上同时标记“部门数据”和“用户数据”两个维度而且不强制所有实体类继承同一个父类。缺点是 AOP 对方法签名有要求被拦截的方法参数中必须存在一个BaseEntity子类否则无法写入 params。新增业务接口时要注意这点。3.3 前端动态路由与按钮级权限后端返回菜单树后前端需要动态往 Vue Router 挂载路由而不是在 router 里写死。常见做法是登录成功后请求/getRouters再调用router.addRouteimport { getRouters } from /api/menu import { generateRoutes } from /store/modules/permission const accessRoutes generateRoutes(await getRouters()) accessRoutes.forEach(route router.addRoute(route))generateRoutes会把后端返回的component字符串映射为.vue文件导入例如system/user/index映射到/views/system/user/index.vue。这一步如果映射失败页面会白屏或一直 loading多数原因是后端菜单表中的 component 字段大小写与磁盘文件名不一致。按钮级别权限用自定义指令v-hasPermiel-button v-hasPermi[system:config:remove]删除/el-button该指令在 mounted 阶段比对store中的权限集合没有权限时直接从 DOM 移除元素。注意它只是展示层控制不能替代后端的接口鉴权真正安全边界由 Spring Security 的方法注解负责。4. 多租户与 Flowable 工作流业务表和服务 API 的协作边界4.1 多租户隔离的策略选择SaaS 场景下租户隔离有独立数据库、共享数据库独立 Schema、共享表加租户字段三种方案。RuoYi-Vue Pro 默认采用第三种所有业务表增加tenant_id字段由 MyBatis Plus 的租户插件在 SQL 执行期自动追加条件。这样可以减少数据源连接池数量成本是开发者必须遵守“所有查询都走 MyBatis Plus 拦截器”的约定。一旦启用多租户所有经过 MyBatis Plus 的查询、插入、修改语句都会自动注入tenant_id ?包括连表查询中的业务表。只有租户表本身、需要全局共享的字典表需要排除。排除表通过ignoreTable方法维护Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); TenantLineInnerInterceptor tenantLine new TenantLineInnerInterceptor(); tenantLine.setTenantLineHandler(new TenantLineHandler() { Override public Expression getTenantId() { // 从当前线程上下文获取租户ID登录时写入 return new LongValue(TenantContext.getTenantId()); } Override public String getTenantIdColumn() { return tenant_id; } Override public boolean ignoreTable(String tableName) { // 忽略系统配置和租户表本身避免递归加载租户数据 return sys_tenant.equalsIgnoreCase(tableName) || sys_config.equalsIgnoreCase(tableName); } }); interceptor.addInnerInterceptor(tenantLine); return interceptor; }getTenantIdColumn决定注入到 SQL 的租户字段名如果租户项目里统一用tenant_id保持默认即可。ignoreTable一定要覆盖sys_tenant本身否则查询租户列表时插件也会给表追加tenant_id条件导致初始化租户时无法读取租户数据。4.2 租户上下文与登录态传递服务端判断当前租户的依据不能是前端传来的 userId而应该是经过认证之后写入请求线程的租户 ID。简单做法是定义TenantContext登录接口通过请求头里的X-Tenant-Id设置当前线程public class TenantContext { private static final ThreadLocalLong CURRENT_TENANT new ThreadLocal(); public static void setTenantId(Long tenantId) { CURRENT_TENANT.set(tenantId); } public static Long getTenantId() { return CURRENT_TENANT.get(); } public static void clear() { CURRENT_TENANT.remove(); } }在 Spring Security 过滤器链中登录成功后将 token 解析出的租户写入上下文请求结束前调用clear()释放。如果使用普通 ThreadLocal并发场景下线程池复用会导致租户串号所以务必在 finally 块中清理。对于异步任务或消息监听可以让TenantContext继承InheritableThreadLocal但注意它只能承接父线程创建时的值线程池复用时同样有脏值风险。前端在登录之后只需要在每次请求的 header 里带上X-Tenant-Id后端网关统一校验。如果网关层已经解析了 token应该在网关过滤器里写入这个 header不要信任客户端自行设置的租户头。4.3 Flowable 服务 API 与业务键绑定工作流模块的核心是把业务流程定义与业务表关联起来。Flowable 提供RepositoryService、RuntimeService、TaskService三个面向业务的 API分别处理流程定义部署、流程实例启动和待办任务。启动流程时代码大致如下Autowired private RuntimeService runtimeService; public String submit(String processKey, String businessKey, MapString, Object variables) { ProcessInstance instance runtimeService .startProcessInstanceByKey(processKey, businessKey, variables); return instance.getId(); }参数中processKey对应 BPMN 文件里process id比如leavebusinessKey推荐直接使用业务表主键如请假单 ID这样在ACT_HI_PROCINST表里可以通过business_key查出完整业务链路。variables用来传递审批变量比如天数、金额、下一批审批人不要放对象进去因为流程变量会序列化到历史表对象过大拖慢流程查询。Fowable Service典型用途对应表RepositoryService部署流程定义、查询 BPMNACT_RE_MODELRuntimeService启动流程、推进流程实例ACT_RU_EXECUTIONTaskService查询并完成用户任务ACT_RU_TASK如果流程结束需要改业务状态常见做法是完成最后一个任务时在 Service 层同步更新业务表状态而不是完全依赖监听器。原因在于监听器里注入自定义 Bean 有过早初始化风险而且当流程变量与租户 ID 同时存在时监听器内的 MyBatis Plus 操作可能取不到租户上下文导致状态更新落到错误租户。5. 部署前必做的验证Actuator 端点与常见坑位排查5.1 用 Actuator 做健康检查并收敛端点Spring Boot Actuator 暴露服务存活状态时默认端点安全策略往往没跟上。先只开放 health 和 info 端点生产环境不要暴露 mappings、beansmanagement: endpoints: web: exposure: include: health,info endpoint: health: show-details: never执行curl -i http://localhost:8080/actuator/health返回{status:UP}即可。如果返回 401说明 Spring Security 未放行该路径需要在过滤链中permitAll放行/actuator/health。其余端点用hasRole(ADMIN)限制避免出 actuator 未授权访问问题。部署时可以直接把健康检查写成 Docker HEALTHCHECKHEALTHCHECK --interval30s --timeout3s \ CMD curl -f http://localhost:8080/actuator/health || exit 15.2 常见坑位速查场景现象处理方式登录后刷新首页空白动态路由 addRoute 未持久化刷新时先用 store 重新初始化 permission 模块多租户开启后 SQL 报列不存在某张表没有 tenant_id 字段在 ignoreTable 中排除该表或给表补列Flowable 流程图中文乱码加载 SVG 时字体缺失容器安装中文字体配置 flowable 字体Vue 接口跨域登录请求 401检查 vue.config.js proxy target 和 pathRewrite生产环境多租户场景下租户表和配置表建议启动时一次性加载到本地缓存避免每次请求都穿透数据库。Redis 和数据库连接池的超时时间也别用默认值至少设置max-wait3000和validation-timeout1000让服务在依赖组件不可用时快速失败而不是让线程池被慢请求占满。本文还有配套的精品资源点击获取
返回列表