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 微服务统一入口
system、infra、bpm 提供平台核心;oa、hrm、crm、erp、finance、asset、project 等按业务域拆分。模块间优先依赖 *-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:菜单、按钮和接口必须使用同一条权限码
核心关系是:
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 模块理解车辆、预算或资产规则。
FlowBillServiceFactory 按 processDefinitionKey 找到对应业务服务。用车申请审批通过后锁定车辆时段,资产领用回写使用人,报销驳回释放预算和发票占用。
在单体模式下可以使用本地事件;微服务模式需要 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 依赖与菜单套餐可以不同。架构支持某模块,不等于每个客户环境都部署了该服务。
十一、上线前检查清单
-
每个写接口都有后端权限码校验;
-
菜单、按钮权限码与 Controller 注解完全一致;
-
业务表包含正确的租户字段,只有全局表才忽略租户;
-
列表、导出、统计和下拉接口都经过数据权限;
-
当前部门切换后,权限缓存键包含组织上下文;
-
RPC、MQ、定时任务和 Redis 缓存正确传播租户;
-
微服务路由与
spring.application.name对齐; -
Gateway 透传身份后,下游仍独立鉴权;
-
流程 key 能路由到正确的
FlowBillService; -
流程通过、拒绝、取消都能回写业务;
-
单体与微服务至少各跑一次核心链路;
-
使用不同租户、不同角色、不同部门账号做越权测试。
常见问题(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 支持一下!**