SpringBoot3+Vue3 企业级管理平台架构:多租户、RBAC、数据权限、工作流与微服务一次讲透

SpringBoot3+Vue3 企业级管理平台架构:多租户、RBAC、数据权限、工作流与微服务一次讲透

🌐 **文档地址**:https://ruoyioffice.com
📦 **源码1·GitHub**:https://github.com/yuqing2026/ruoyi-office
📦 **源码2·GitCode**:https://gitcode.com/zhouzhongyan/ruoyi-office
📦 **源码3·Gitee**:https://gitee.com/yqzy1688/ruoyi-office
💬 **微信**:17156169080(备注「RuoYi Office」)
企业级后台最危险的错觉,是"登录成功、左侧有菜单"就等于权限和架构已经完成。真正上线后会同时遇到五个问题:这个人是谁?数据属于哪家公司?他能操作哪些功能?他能看哪些行?业务单据审批后如何回到业务台账?如果系统还要支持单体和微服务两种部署,这些上下文又怎样跨网关、RPC、缓存和消息队列传递?

*▲ 一条业务请求要依次经过客户端、网关、身份与功能权限、租户和数据权限,最后才进入业务与工作流*


引言:先回答权限四问,再谈模块有多少

一个企业管理平台可以有 OA、HRM、CRM、ERP、财务、资产和项目等几十个模块,但架构底座必须先回答四个问题:

text 复制代码
谁登录?       → Spring Security + OAuth2 Token
属于哪家?     → tenant-id + 租户上下文
能做什么?     → RBAC 菜单、按钮与接口权限
能看哪些行?   → DataScope + SQL 改写

工作流则解决第五个问题:一张业务单如何按规则流转,并把最终结果回写到车辆、合同、员工、资产、库存或财务台账。

本文不分别重写一篇 RBAC、一篇多租户和一篇微服务教程,而是沿着一条 HTTP 请求,观察这些横切能力如何叠加。


一、总体分层:框架 Starter 承载横切能力,业务模块保持边界

后端不是一个巨大的 service 目录,而是三层结构:

text 复制代码
****-framework
  ├─ security / tenant / data-permission
  ├─ web / mybatis / redis / rpc / mq
  └─ 通用自动配置 Starter

****-module-xxx
  ├─ xxx-api      对外 DTO、枚举、RPC 契约
  └─ xxx-server   Controller、Service、Mapper、业务实现

启动层
  ├─ ****-server   单体聚合入口
  └─ ****-gateway  微服务统一入口

systeminfrabpm 提供平台核心;oahrmcrmerpfinanceassetproject 等按业务域拆分。模块间优先依赖 *-api 契约,不直接穿透到另一个模块的 Mapper。

*▲ 用户看到的是一个统一工作台,底层仍按系统、流程和业务域保持模块边界*

1.1 为什么横切能力要做成 Starter

如果每个模块分别实现 Token 解析、租户过滤和数据权限,规则很快会漂移:OA 忘了过滤租户,CRM 的部门权限又和 HRM 不一致。Starter 让每个 *-server 使用同一套安全和 SQL 插件链。

业务模块只需要声明:

java 复制代码
@PreAuthorize("@ss.hasPermission('hrm:employee:query')")
@DataPermission
public PageResult<EmployeeDO> getEmployeePage(EmployeePageReqVO reqVO) {
    return employeeMapper.selectPage(reqVO);
}

真正的 Token、权限码、租户 ID 和部门范围解析由框架完成。


二、部署双模式:同一套模块代码,先单体还是直接微服务

根 Maven 工程提供两种 Profile:

| 模式 | 命令 | 运行形态 | 适合场景 |

|:---|:---|:---|:---|

| boot | mvn -P boot ... | ****-server 单进程聚合模块 | 本地开发、中小规模、快速交付 |

| cloud | mvn -P cloud ... | Gateway + 各 *-server 独立部署 | 团队分工、弹性扩容、服务治理 |

cloud 是默认 Maven Profile,但不代表生产必须微服务。小团队把二十个服务全部拆开,会增加 Nacos、链路追踪、配置、日志、发布顺序和资源成本。先用单体把业务边界写清,再按压力和团队边界拆服务,通常更稳。

*▲ 微服务增加统一入口、注册配置和服务调用;业务模块本身仍保持同一套 API 与实现边界*

2.1 Gateway 负责什么

微服务模式下,请求进入 Gateway:

text 复制代码
/admin-api/system/**  → system-server
/admin-api/bpm/**     → bpm-server
/admin-api/oa/**      → oa-server
/admin-api/hrm/**     → hrm-server

路由使用 Nacos 服务发现和 grayLb 负载策略。网关的 Token 过滤器可以解析访问令牌并向下游透传 login-user,其中包含 userId、tenantId、deptId 等上下文。

需要诚实说明:网关不是唯一安全边界。无效或缺失 Token 的请求可能继续转发,最终由下游 Spring Security、@PermitAll 和方法权限决定返回 401 或 403。这样单体模式和绕过网关的内部调用也不会失去鉴权。


三、身份层:默认不是 JWT,而是可撤销的不透明 Token

登录成功后,系统签发 OAuth2 Access Token 和 Refresh Token。默认 Token 是不透明标识,服务端在数据库保存并通过 Redis 加速,而不是把全部身份信息永久塞进 JWT。

text 复制代码
用户名 / 短信 / 社交登录
  → AdminAuthService
  → OAuth2TokenService
  → access_token + refresh_token
  → MySQL 持久化 + Redis 热缓存

不透明 Token 的代价是需要服务端查询;优势是可以踢人、撤销、续期、查看在线状态,并及时反映用户禁用和租户过期。

服务内 TokenAuthenticationFilter 把身份放入 Spring Security Context。随后 Controller 使用方法级权限:

java 复制代码
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('system:user:query')")
public CommonResult<PageResult<UserRespVO>> getUserPage(
        @Valid UserPageReqVO pageReqVO) {
    return success(userService.getUserPage(pageReqVO));
}

登录只证明"你是谁",这条注解才开始回答"你能不能做这件事"。


四、RBAC:菜单、按钮和接口必须使用同一条权限码

核心关系是:

erDiagram SYSTEM_USER ||--o{ SYSTEM_USER_ROLE : has SYSTEM_ROLE ||--o{ SYSTEM_USER_ROLE : assigned SYSTEM_ROLE ||--o{ SYSTEM_ROLE_MENU : owns SYSTEM_MENU ||--o{ SYSTEM_ROLE_MENU : granted SYSTEM_ROLE { bigint id string code int data_scope json data_scope_dept_ids } SYSTEM_MENU { bigint id int type string permission bigint parent_id }

system_menu 同时承载目录、页面菜单和按钮节点。按钮节点的 permission 例如:

text 复制代码
system:user:query
system:user:create
system:user:update
system:user:delete

角色选择菜单树时也会选中按钮节点;后端 PermissionServiceImpl 根据用户角色、角色菜单和权限字符串做判断。

4.1 前端如何拿到同一份权限

登录后,auth.ts 调用 /system/auth/get-permission-info,一次拿回:

  • 用户信息;

  • 角色集合;

  • 后端菜单树;

  • 按钮权限码集合。

菜单树经 Vben 的 generateAccessible() 转成动态路由;按钮使用:

vue 复制代码
<Button v-access:code="['system:user:create']">
  新增用户
</Button>

或在脚本中调用 hasAccessByCodes()

前端隐藏按钮只是在改善体验。攻击者可以绕过页面直接调用 API,所以后端 @PreAuthorize 必须使用同一权限码再次校验。

4.2 菜单不存在时为什么要严格拒绝

若 Controller 写了 @ss.hasPermission('foo:bar:create'),但菜单表没有这个权限码,宽松实现可能误把它当"未配置所以放行"。当前严格模式会直接拒绝,迫使开发和实施把菜单 SQL、角色授权与代码注解对齐。


五、数据权限:有查询按钮,不等于能看全公司的数据

RBAC 判断"能不能查员工";DataScope 判断"能查哪些员工"。两者正交。

常见五档数据范围:

| 范围 | SQL 语义 |

|:---|:---|

| 全部数据 | 不追加部门或本人条件 |

| 指定部门 | dept_id IN (...) |

| 本部门 | dept_id = currentDeptId |

| 本部门及子部门 | dept_id IN (deptTree) |

| 仅本人 | user_id = loginUserId |

*▲ 角色不仅分配菜单,还要决定可见数据范围;指定部门可以选择组织树节点*

MyBatis-Plus 查询进入 DataPermissionRuleHandler 后,由 DeptDataPermissionRule.getExpression() 生成条件并追加到 SQL。

sql 复制代码
-- 业务代码原查询
SELECT * FROM hrm_employee WHERE status = 1;

-- 当前用户只有研发部及子部门权限
SELECT * FROM hrm_employee
WHERE status = 1
  AND dept_id IN (12, 18, 23);

5.1 数据权限为什么不能只写在 PageReqVO

让前端传 deptId 只能实现筛选,不能实现安全。用户可以删掉参数或改成其它部门。数据范围必须来自登录身份和角色,在 SQL 层自动追加。

5.2 什么时候需要关闭数据权限

登录初始化、权限计算等少数内部查询如果也被 DataScope 过滤,可能导致"为了算权限先需要权限"的循环。此时可以使用 @DataPermission(enable = false),但必须限定在清楚的基础设施方法上,不能因为查询结果少就随手关闭。


六、多租户:先隔离公司,再在公司内部做数据权限

默认多租户方案是共享数据库、共享表、每行业务数据带 tenant_id。请求头中的租户 ID 经 TenantContextWebFilter 写入 TenantContextHolder,MyBatis-Plus 的 TenantDatabaseInterceptor 自动追加条件。

sql 复制代码
-- 先隔离租户,再叠加部门数据范围
SELECT * FROM hrm_employee
WHERE tenant_id = 1024
  AND dept_id IN (12, 18, 23)
  AND status = 1;

顺序上可以理解为:

text 复制代码
租户边界:先确认是哪家公司
  AND
数据权限:再确认这个人在公司内能看哪些部门

实体继承 TenantBaseDO 后具备租户字段;全局菜单等不应按租户重复的数据使用 @TenantIgnore。此外,租户上下文还要进入 Redis、RPC、定时任务和消息队列,不能只在 Web 请求里有效。

6.1 租户套餐不是一张价格表

新租户初始化时,套餐决定可用菜单、流程模型、默认角色和组织骨架。它解决的是"这家公司购买了哪些能力",角色解决的是"公司里的这个人能用哪些能力"。

*▲ 套餐先裁出租户可用模块,租户内部再通过角色进行二次授权*

6.2 多租户和多组织不是一回事

  • 多租户切换:进入另一家逻辑隔离的公司,tenant_id 变化;

  • 多组织切换:仍在同一租户内切换当前任职、部门或公司主体,deptId 变化。

顶栏切换任职会影响部门负责人、数据权限和流程候选人,但不会跨出租户边界。把两者混为一谈容易产生越权。


七、工作流:权限决定谁能办,Flowable 决定单据往哪走

当前 BPM 模块基于 Flowable 8.0.0,负责模型、流程实例、任务、候选人和历史轨迹。业务模块不应直接修改 Flowable 运行表,也不应让 BPM 模块理解车辆、预算或资产规则。

sequenceDiagram participant B as 业务模块 participant P as BPM 模块 participant F as Flowable participant E as 状态事件 B->>P: 发起流程(processKey, businessKey) P->>F: 启动已发布定义 F-->>P: 创建并推进任务 P-->>B: processInstanceId F->>P: 流程结束 P->>E: 发布审批结果 E->>B: FlowBillService.updateProcessStatus B->>B: 回写业务台账与资源状态

FlowBillServiceFactoryprocessDefinitionKey 找到对应业务服务。用车申请审批通过后锁定车辆时段,资产领用回写使用人,报销驳回释放预算和发票占用。

在单体模式下可以使用本地事件;微服务模式需要 MQ、HTTP 或 RPC 传递状态。无论部署方式如何,业务契约不变。

7.1 BPM 与权限在哪里交叉

  • 流程发起权限决定谁能看到并启动模型;

  • 节点候选人结合部门、角色、岗位和发起人上下文;

  • 节点字段权限决定审批人能改哪些业务字段;

  • 按钮权限决定是否允许通过、退回、转办或加签;

  • DataScope 继续约束业务台账查询。

工作流不是越过 RBAC 和租户的另一个入口,而是建立在它们之上。


八、一条员工列表请求如何穿过全栈

以"研发部主管打开员工列表"为例:

第一步:客户端带上下文

Vue3 发送:

text 复制代码
Authorization: Bearer <access-token>
tenant-id: 1024
current-dept-id: 12
GET /admin-api/hrm/employee/page

第二步:Gateway 路由

微服务模式下,Gateway 根据 /admin-api/hrm/找到 hrm-server,解析 Token 并透传登录上下文;单体模式则直接进入 **-server

第三步:服务内认证与功能权限

Token Filter 构造 LoginUser@PreAuthorize("@ss.hasPermission('hrm:employee:query')") 校验角色是否拥有查询按钮权限。

第四步:SQL 叠加两层隔离

Tenant 插件追加 tenant_id = 1024,DataPermission 插件再追加研发部及子部门范围。即使用户手改前端部门筛选,也不能读到其它租户或无权部门。

第五步:前端只展示允许的操作

响应返回后,页面根据 accessCodes 决定是否显示新增、编辑、导出等按钮。后端依旧对每个接口单独鉴权。

这条链解释了为什么"同一个页面,不同账号看到的数据和按钮都不同",也解释了排障时应逐层检查 Token、租户、权限码和 DataScope,而不是只改前端。


九、四类常见架构事故

9.1 前端隐藏按钮,接口没有 `@PreAuthorize`

结果:普通用户在浏览器控制台直接调用删除接口。修复位置在 Controller 权限,不是把按钮藏得更深。

9.2 业务表忘记租户字段或被错误忽略

结果:列表可能读到其它客户数据。应检查实体是否继承租户基类、表是否属于全局表、SQL 插件是否经过,而不是在每个 Mapper 手写 tenant 条件。

9.3 报表或自定义 SQL 绕过数据权限

复杂 JOIN、报表数据集和手工 SQL 需要明确别名与部门字段映射。查得快但越权,仍是严重缺陷。

9.4 流程结束,业务状态没有回写

Flowable 显示已通过,但业务单仍是审批中,资源也没有锁定或释放。应检查流程 key、FlowBillService 路由和状态事件,不要在前端根据流程颜色猜业务状态。


十、架构选型边界:哪些话不能说满

10.1 多租户默认是共享表方案

独立数据库和独立 Schema 可以扩展,但当前主路径是 tenant_id 行级隔离。不要宣传成三种模式全部开箱切换。

10.2 微服务不是越多越高级

系统支持 boot/cloud 双模式。是否拆分取决于访问量、团队边界、发布频率和运维能力,不取决于模块数量。

10.3 默认 Token 不是 JWT

不透明 Token 依赖服务端存储,但换来实时撤销和在线管理能力。若改成 JWT,要重新评估续期、踢人和权限变更生效策略。

10.4 前端权限不是安全边界

动态路由和 v-access 只能减少误操作,真正的安全仍在方法权限、租户插件和数据权限插件。

10.5 所有模块不一定在每个版本启用

全功能版、OA 版和可选业务模块的 Maven 依赖与菜单套餐可以不同。架构支持某模块,不等于每个客户环境都部署了该服务。


十一、上线前检查清单

  1. 每个写接口都有后端权限码校验;

  2. 菜单、按钮权限码与 Controller 注解完全一致;

  3. 业务表包含正确的租户字段,只有全局表才忽略租户;

  4. 列表、导出、统计和下拉接口都经过数据权限;

  5. 当前部门切换后,权限缓存键包含组织上下文;

  6. RPC、MQ、定时任务和 Redis 缓存正确传播租户;

  7. 微服务路由与 spring.application.name 对齐;

  8. Gateway 透传身份后,下游仍独立鉴权;

  9. 流程 key 能路由到正确的 FlowBillService

  10. 流程通过、拒绝、取消都能回写业务;

  11. 单体与微服务至少各跑一次核心链路;

  12. 使用不同租户、不同角色、不同部门账号做越权测试。


常见问题(FAQ)

RBAC 和数据权限有什么区别?

RBAC 决定能否调用"员工查询"功能;数据权限决定查询结果是全公司、本部门、子部门还是仅本人。两者缺一不可。

多租户已经隔离数据,还需要 DataScope 吗?

需要。租户只隔离不同客户;同一客户内部仍有部门、岗位和本人数据边界。

为什么前后端要使用相同权限码?

前端据此控制页面体验,后端据此执行安全校验。两边不一致会出现按钮可见但接口 403,或按钮隐藏但接口可越权。

小团队应该直接上微服务吗?

通常不必。先用 boot 单体保持模块边界,业务和团队规模需要时再切 cloud,可以降低早期运维成本。

Gateway 验证过 Token 后,下游还需要 Spring Security 吗?

需要。服务可能被内部调用或在单体模式运行,接口权限也比"是否登录"更细。网关负责统一入口和身份透传,下游负责最终授权。


结语

企业级管理平台架构不是把 Spring Security、MyBatis-Plus、Flowable 和 Nacos 列进技术栈,而是让它们围绕同一条请求形成稳定边界:

text 复制代码
Token 证明身份
→ tenant_id 隔离客户
→ RBAC 校验功能
→ DataScope 限制数据行
→ 业务服务执行规则
→ Flowable 推动审批并回写业务

再通过 boot/cloud 双模式,让同一套模块代码既能低成本单体交付,也能在规模化后拆成微服务。真正值得复用的不是模块数量,而是这些横切能力在每个业务域都执行一致。


💡 **想要体验 RuoYi Office 的强大功能?**

>

🌐 **在线演示**:https://ruoyioffice.com/web/(账号 admin / admin123)

>

📦 **源码仓库**:GitHub:https://github.com/yuqing2026/ruoyi-office | GitCode:https://gitcode.com/zhouzhongyan/ruoyi-office | Gitee:https://gitee.com/yqzy1688/ruoyi-office

>

💬 **技术咨询**:添加微信 **17156169080**,备注「RuoYi Office」

>

⭐ **如果觉得不错,请给个 Star 支持一下!**

相关推荐
砍材农夫2 小时前
spring|spring web|拦截器、过滤器、aop
java·spring boot·spring·spring cloud
weixin_BYSJ198710 小时前
【计算机毕设】基于SpringBoot与Vue的文物保护档案管理系统08621
vue.js·spring boot·spring cloud·微服务·架构·django·课程设计
Bs_MoneyMagnet11 小时前
基于springboot+vue的个人健康管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·vue3·springboot3·计算机毕业设计
Bs_MoneyMagnet14 小时前
基于springboot+vue的心理咨询预约与随访平台的设计与实现 源码+文档
vue.js·spring boot·后端·spring·毕业设计·旅游·计算机毕业设计
陌シ未央ゞ15 小时前
基于BM25算法和RRF实现的混合索引(java版)
人工智能·spring boot·后端·算法
user_admin_god18 小时前
第 01 篇:OpenAI 兼容 API 初探 —— 用 curl 跑通第一次对话
java·人工智能·spring boot·语言模型·devops
小辰爱喝汤20 小时前
基于智能定价的 校园二手交易平台-计算机毕业设计
spring boot·毕业设计·课程设计
志尊宝21 小时前
Vue3 零基础每日笔记(026):emit 子传父——defineEmits 与事件校验
前端·javascript·html·vue3
RuoyiOffice21 小时前
SpringBoot3+Vue3 库存盘点:账面和实盘对不上,差异单怎么走审批再调账
spring boot·vue3·erp·spring boot 3·库存盘点·盘盈盘亏