ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot系统管理模块开发指南:RBAC权限模型与全流程落地实践

2026/10/7 17:00:39 拓冰建站 浏览量
Spring Boot系统管理模块开发指南:RBAC权限模型与全流程落地实践 做后端开发这些年我接手过不下二十个企业级项目几乎每个项目里都有这么一套东西用户管理、角色管理、菜单管理、部门管理、操作日志。这套东西在业内有个统一的名字——系统管理模块。不管你用 Spring Boot 还是其他框架不管做电商后台还是办公系统系统管理模块永远是第一个被拉起来、最后一个被打磨完备的模块。系统管理模块背后藏着的是一整套权限控制思路它决定了整个项目里谁能看到什么、谁能操作什么、出了问题能追到谁头上。这是后端项目里最基础却又最绕不开的一块也是很多初级开发最容易写出能用但跑不了生产代码的地方。这篇文章我会从模块拆解、RBAC 权限模型、核心功能落地、前后端联调、日志审计到问题排查把这块内容完整梳理一遍。刚接触后端开发、准备做毕设或公司内部项目的读者可以照着把骨架搭起来已经写过类似模块的开发者建议重点关注第四、第六部分那是我踩过坑之后整理出来的细节。1. 系统管理模块到底在管什么1.1 模块全景拆解系统管理模块不是一个单一功能而是一组相互关联的子功能集合。以国内使用率最高的 RuoYi若依框架为例它对系统管理模块的划分非常清晰基本代表了行业内的标准做法子模块核心作用常见实现要点用户管理维护系统所有登录账号增删改查、分配角色、重置密码、状态启停角色管理定义权限集合方便批量授权创建角色、给角色分配菜单权限菜单管理维护前端导航和按钮权限点树形结构、增删改查、排序部门管理维护组织架构树形结构、负责人、状态岗位管理管理职位信息与用户关联、简单 CRUD字典管理统一维护下拉选项等枚举数据字典类型加字典数据前后端联动参数管理维护系统级配置key-value 形式支持缓存操作日志记录谁在什么时候做了什么AOP 加注解、异步落库登录日志记录登录行为和结果记录 IP、时间、成功失败这里有个很容易被忽略的点字典管理。很多新手觉得字典就是几张表不重视。但实际项目里前端的性别下拉框、状态开关、业务类型选择全部来自字典。字典设计得好业务代码里写死的硬编码值会少很多后面维护起来轻松一个量级。我见过一个项目把状态值散落在二十多个类里每次改动状态枚举都要全局搜索替换就是因为当初偷懒没建字典表。1.2 为什么几乎所有项目都离不开这套系统管理模块的本质是认证和授权这套基础设施的业务化表达。任何一个多人使用的系统必须解决三个问题你怎么证明你是你——登录认证。你登录之后能看什么、能点什么——权限判断。你做了什么事出了问题能不能回溯——行为审计。这三个问题直接对应系统管理模块的功能设计。所以你会发现不管技术栈怎么变只要是个正经的企业项目这套东西一定存在只是叫法不同有的团队叫权限中心有的叫后台管理有的直接拆成独立服务但内核一致。理解了这一点再看这个模块就不会觉得它是凑功能的 CRUD而是一套安全基础设施的外壳。2. RBAC权限模型系统管理模块的地基2.1 从门禁卡说起的RBAC原理RBACRole-Based Access Control基于角色的权限控制是系统管理模块最主流的权限模型。理解它有个生活化类比公司大楼的门禁系统。你入职时人事不会直接给你配每扇门的钥匙。她会发你一张员工卡用户然后在系统里设置一个研发工程师角色。这个角色自带门禁权限能刷开研发部的门、会议室的门但进不了财务室。哪天你转岗了人事只需要把角色换成产品经理门禁权限就全部变了不需要重新配卡。对应到系统里就是用户表你是谁。角色表你被赋予了什么身份。菜单权限表每种身份能访问哪些资源。用户角色关联表一个人可以有多重身份。角色菜单关联表一个角色拥有哪些权限点。这套模型最大的好处是解耦。权限管理粒度从单个用户提升到角色维度新增员工不需要从零配置权限选中角色就完成了授权。这也是为什么中小型项目几乎清一色用 RBAC而复杂组织才会考虑 ABAC基于属性的权限控制。如果你的项目角色数量超过几百个或者权限规则涉及时间、部门、数据范围等动态条件再来研究 ABAC 也不迟普通项目 RBAC 完全够用。2.2 数据库表怎么设计直接给一套我在生产环境用过的建表核心逻辑。以 MySQL 为例五张核心表的字段设计重点如下-- 用户表 CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 加密后的密码, nickname VARCHAR(30) COMMENT 昵称, email VARCHAR(50) COMMENT 邮箱, phone VARCHAR(11) COMMENT 手机号, status CHAR(1) DEFAULT 0 COMMENT 状态 0正常 1停用, del_flag CHAR(1) DEFAULT 0 COMMENT 删除标志 0存在 2删除, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT用户表; -- 角色表 CREATE TABLE sys_role ( id BIGINT AUTO_INCREMENT PRIMARY KEY, role_name VARCHAR(30) NOT NULL COMMENT 角色名称, role_key VARCHAR(50) NOT NULL COMMENT 角色权限字符, status CHAR(1) DEFAULT 0 COMMENT 状态, remark VARCHAR(255) ) ENGINEInnoDB COMMENT角色表; -- 菜单权限表 CREATE TABLE sys_menu ( id BIGINT AUTO_INCREMENT PRIMARY KEY, parent_id BIGINT DEFAULT 0 COMMENT 父菜单ID, menu_name VARCHAR(50) NOT NULL COMMENT 菜单名称, menu_type CHAR(1) COMMENT 类型 M目录 C菜单 F按钮, path VARCHAR(200) COMMENT 路由地址, perms VARCHAR(100) COMMENT 权限标识, sort_order INT DEFAULT 0, status CHAR(1) DEFAULT 0 ) ENGINEInnoDB COMMENT菜单权限表; -- 用户角色关联表 CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ) ENGINEInnoDB COMMENT用户角色关联表; -- 角色菜单关联表 CREATE TABLE sys_role_menu ( role_id BIGINT NOT NULL, menu_id BIGINT NOT NULL, PRIMARY KEY (role_id, menu_id) ) ENGINEInnoDB COMMENT角色菜单关联表;这里有两个重要细节。一是逻辑删除字段del_flag。生产环境千万别做物理删除否则以后审计和统计全废了。很多新人入职第一周就把物理 DELETE 写进业务代码我每次 code review 看到都想拍桌子。二是外键问题。这套表刻意没有写物理外键因为互联网场景下高并发写入时外键会成为性能瓶颈而且导致删除操作极其困难。现在的行业共识是应用层保证关联逻辑数据库层不加物理外键。如果你习惯了用外键做级联删除请记住这个习惯在大型项目里要改掉关联关系的清理应该在 Service 层的同一个事务里显式完成。2.3 菜单权限跟接口权限的区别很多人搞不清一个问题菜单管理和接口权限到底是什么关系菜单表里有一列perms权限标识比如system:user:add表示新增用户这个操作。用户登录后系统根据该用户的角色把所有权限标识收集起来存入 Redis 或内存。每当用户请求一个接口后端拦截器拿到接口上标注的perms与用户拥有的权限集合比对有权限就放行没有就返回 403。所以菜单管理在前端的作用是控制你看得见什么接口上的权限标识在后端的作用是控制你能调用什么。前端隐藏菜单只是用户体验层面的保护真正拦人的永远在后端。这个原则在前后端分离项目里是权限安全的核心后面还会再强调。3. 后端核心功能落地实操3.1 用户管理不只是增删改查用户管理是系统管理模块里业务最丰富的子模块。除了常规 CRUD还有四个关键动作新增用户时分配角色、重置密码、修改用户状态、查询用户时关联展示角色信息。新增用户的接口实现我建议采用先落用户、再绑角色的两步事务写法Transactional(rollbackFor Exception.class) public void addUser(SysUser user, Long[] roleIds) { // 1. 校验用户名是否重复 if (userMapper.selectByUsername(user.getUsername()) ! null) { throw new ServiceException(用户名已存在); } // 2. 对密码做 BCrypt 加密 user.setPassword(passwordEncoder.encode(user.getPassword())); userMapper.insert(user); // 3. 绑定角色 if (roleIds ! null roleIds.length 0) { userRoleMapper.insertByUser(user.getId(), roleIds); } }这里两个细节非常关键。第一是事务注解用户表和关联表必须同生共死否则会出现用户建好了但角色没绑上的脏数据。第二是密码加密必须在新用户创建时就做如果做成保存明文、登录时再加密比对迟早出安全事故。重置密码这个功能也别小看。它通常分两种情况管理员给用户重置密码以及用户自己修改密码。重置密码一般生成一个随机初始密码然后强制用户首次登录修改;修改密码则必须校验原密码。我遇到过接口设计没考虑校验原密码的项目任何拿到登录态的人都能直接改密码配合一个 XSS 漏洞简直是一场灾难。3.2 密码安全为什么必须用 BCrypt我见过太多项目里密码直接 MD5 存库甚至明文存库。说清楚一个事实MD5 或 SHA256 这类哈希算法速度太快在 GPU 算力下暴力破解非常容易。密码存储的正确姿势是用 BCrypt。BCrypt 有三个特性内置随机盐、可调计算强度、相同密码每次加密结果不同。两个用户密码相同数据库里存的两个哈希值也不一样极大提高了脱库后的破解难度。Spring Security 里直接引入即可Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }校验时调用passwordEncoder.matches(rawPassword, encodedPassword)。登录逻辑里对比密码之后常规操作是用工具类生成一个 Token 返回前端。Token 可以是一个 UUID 存 Redis 做分布式会话也可以是 JWT 做无状态令牌。中小项目我倾向 Redis 加 Token因为可以随时踢人下线、控制失效时间比 JWT 的黑名单机制舒服很多。关于 Token 过期时间我建议 access token 短一些比如 2 小时配合前端定时刷新。一次性给个 7 天有效期的 Token虽然用户省事了但泄露后的风险窗口也拉长了。安全性和易用性之间后端要主动做取舍而不是把选择权丢给产品经理拍脑袋。3.3 角色与菜单绑定小心递归的坑菜单管理是典型的树形结构业务。菜单表通过parent_id自关联形成多级树。查询菜单树有两种常见方式方式一递归查询数据库。先查一级菜单再逐层查询子菜单。代码好写但数据量大时会产生 N 次查询接口耗时指数级上升。菜单表通常几十条数据还好但树深了就有隐患。方式二一次性查出全表在内存里组装树。只发一条 SQL把整张表查出来然后在 Java 层通过分组拼成树。我推荐这种方式实现也不复杂public ListMenuTreeVO buildTree(ListSysMenu menus) { MapLong, ListMenuTreeVO childMap menus.stream() .map(this::toVO) .collect(Collectors.groupingBy(vo - vo.getParentId())); return childMap.getOrDefault(0L, Collections.emptyList()).stream() .peek(vo - vo.setChildren(childMap.getOrDefault(vo.getId(), Collections.emptyList()))) .collect(Collectors.toList()); }注意组装树之前要先把当前用户可见的菜单过滤出来。用户能看到哪些菜单取决于他角色关联了哪些菜单这个过滤要在数据库端或组装前做好避免把不该展示的菜单漏给前端。另外菜单表和部门表都用了parent_id这种结构SQL 里查询时一定要注意parent_id为 NULL 和 0 的区别建表时把默认值设成 0否则后续所有树查询都要加OR IS NULL条件麻烦不断。3.4 分页查询MyBatis-Plus 还是 PageHelper用户列表、角色列表这类查询基本都需要分页。国内两种主流做法PageHelper 基于拦截器用 ThreadLocal 传递分页参数使用简单但多线程环境要注意参数泄漏MyBatis-Plus 分页插件官方支持配合IService的page方法很顺手。在 Spring Boot 3 项目里我更推荐 MyBatis-Plus。举个例子public TableDataSysUser list(SysUserQuery query) { LambdaQueryWrapperSysUser wrapper Wrappers.lambdaQuery(); wrapper.like(StringUtils.hasText(query.getUsername()), SysUser::getUsername, query.getUsername()) .eq(StringUtils.hasText(query.getStatus()), SysUser::getStatus, query.getStatus()); PageSysUser page userMapper.selectPage( new Page(query.getPageNum(), query.getPageSize()), wrapper); return TableData.build(page); }这里有个实用细节查询参数接收建议单独写一个 Query 对象不要直接拿实体类接收前端参数。实体类里可能有delFlag、password这类字段直接接收会导致参数覆盖漏洞。你不希望用户通过构造请求把password字段塞进来吧这是很初级但后果很严重的接口安全问题我曾经在 code review 里见过实体类直接当 VO 用一查果然能通过参数修改登录密码。4. 前后端分离下的联调要点4.1 接口设计规范别让前端猜系统管理模块的接口每天被前端反复调用接口设计混乱会直接拖累联调效率。我的个人约定如下统一返回体{ code: 200, msg: 操作成功, data: { } }分页数据固定结构{ total: 100, rows: [] }所有写操作 POST查询操作 GET删除用 DELETE 并传 id。别搞用 GET 传参删除这种邪门写法也别说改就改接口字段。接口文档用 OpenAPI 规范生成前后端契约以文档为准。Apifox 这类工具的 Mock 能力可以用起来让前端在后端未完成时先联调把等待时间省下来。接口命名上我一向建议直接沿用 RuoYi 那套风格/system/user/list、/system/role/list、/system/menu/treeselect。这套命名被大量项目验证过语义清晰前端路由和权限标识都能对齐。如果团队没有自己的命名规范参照它是成本最低的选择。4.2 跨域问题前后端分离的第一道坎前后端分离部署后跨域几乎是必然遇到的问题。前端跑在 8080后端跑在 8081浏览器发起请求时会因为同源策略被拦截。Spring Boot 里的标准解法是配置 CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意两个点一是想支持跨域携带 Cookie 凭证时allowedOrigins不能写通配符要用allowedOriginPatterns二是务必处理 OPTIONS 预检请求。很多新手发现前端报跨域错误第一反应是加配置加了还报错十有八九是预检请求被拦截器拦了没放行 OPTIONS 方法。生产环境最好把allowedOriginPatterns收敛成具体的域名列表别图省事一直开着通配符。4.3 按钮级权限的鉴权链路前端经常要做按钮级别的显隐控制。比如新增用户按钮只有拥有system:user:add权限的人才看得见。前端在登录后把权限标识列表存起来用自定义指令v-permission[system:user:add]判断显隐。但请记住刚才说的前端隐藏只是面子后端拦截才是里子。后端接口上用PreAuthorize做权限校验PreAuthorize(ss.hasPermi(system:user:add)) PostMapping(/user) public AjaxResult add(RequestBody SysUser user) { return success(userService.addUser(user)); }这个ss.hasPermi()是自定义 Bean内部拿当前登录用户的权限集合和权限标识匹配。权限集合哪里来登录时从数据库查出该用户所有角色的所有菜单perms去重后放到 Redis 缓存。后续每次请求只从缓存取性能不会差。实际项目里还有一个常见需求超级管理员。通常用admin这个固定角色或固定用户 ID 判断超级管理员直接绕过所有权限校验。这个逻辑要放在框架里显式处理别到处散落if (admin.equals(...))这种判断不然以后想调整超级管理员的判定规则就是一场全局改造。4.4 重复提交前端后端必须双管齐下系统管理模块里用户连续点保存按钮是最高频的误操作场景。重复提交轻则产生重复数据重则造成关联关系错乱。前端方案最简单提交时把按钮禁用请求结束后恢复。但前端防不住绕过浏览器的恶意请求所以后端也要做幂等控制。我推荐的方案是Token 令牌加 Redis进入表单页面时后端生成一个唯一 token 返回前端前端提交时带着这个 token后端执行业务前尝试从 Redis 删除该 token只有删除成功才放行。Redis 删除操作是原子的并发请求里只有一个能成功其余失败并提示请勿重复提交public T T preventDuplicateSubmit(String token, SupplierT supplier) { Boolean success redisTemplate.delete(submit:token: token); if (Boolean.TRUE.equals(success)) { return supplier.get(); } throw new ServiceException(请勿重复提交); }这个方案的关键是 token 必须一次性使用。前端拿到 token 后只有第一次提交能带上。我的项目经验是核心写操作一定要加这个防护尤其是角色授权、字典更新这类容易被多人同时操作的功能。还有人问为什么不用数据库唯一索引防重我想说的是这两者定位不同唯一索引防的是数据层重复令牌方案防的是请求层重复前者拦不住接口被恶意刷 N 次的场景。5. 日志与审计被低估的系统管理能力5.1 操作日志的 AOP 实现操作日志的价值平时看不见出问题时它是唯一的救命稻草。用户说我昨天明明改了这个配置结果系统被改乱了没有日志你连是谁下的手都查不出来。系统管理模块里的操作日志标准实现是自定义注解加 Spring AOP。定义注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OperLog { String module() default ; String action() default ; }然后写切面类在目标方法执行成功后记录操作信息Aspect Component public class OperLogAspect { Around(annotation(operLog)) public Object around(ProceedingJoinPoint pjp, OperLog operLog) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); long cost System.currentTimeMillis() - start; // 异步保存日志操作人、操作模块、请求参数、响应结果、耗时、IP logService.save(buildLog(pjp, operLog, result, cost)); return result; } }记录参数时注意不要直接把请求体里的密码、Token 等敏感字段打印出来否则信息泄露就是日志系统的锅。我的做法是参数序列化前先过滤敏感 key。日志入库最好是异步的用一个线程池或消息队列处理不然每次操作都同步写库接口耗时明显上升。切面里还要考虑异常情况方法抛出异常时也要记录日志但标记为失败状态这样才能还原完整的操作链路。5.2 登录日志安全的第一道哨兵登录日志要记录登录用户名、登录时间、登录 IP、操作系统、浏览器类型、登录结果。失败次数过多时要配合账号锁定或验证码校验防止暴力破解。这里特别提醒一个细节登录日志和操作日志不要混在一张表里。登录日志写入频率高、数据结构简单建议单独建表或走专门的日志采集通道。我在一个项目里把两者混存最后日志表涨到几百万行时按条件分页查询变得奇慢无比拆表之后才解决。另外登录日志里记录 IP 时要考虑代理场景用X-Forwarded-For头时要取第一个非 unknown 的 IP并且注意伪造问题内网场景至少能定位到出口网关。5.3 日志别乱打分级与保留策略生产环境的日志是烫手山芋。打少了排查不了问题打多了磁盘告警。我的经验是操作日志保留 6 个月定期归档。系统运行日志按级别控制DEBUG 只在测试环境开生产用 INFO 加 WARN 加 ERROR。敏感操作删除用户、修改权限、重置密码必须记录操作前后的值。有个合作团队上线半年后磁盘爆掉一查是操作日志里把每次查询参数都存了一个超长 JSON 塞进字段。这就是设计日志时没想清楚什么该记、什么不该记的代价。日志字段里该存的是业务单据号、操作对象 ID、操作前后的关键字段值而不是整个请求体。6. 常见问题与排查技巧实录6.1 树形菜单组装出来丢失层级现象前端渲染菜单树时多级菜单显示不全或者子菜单挂到了错误节点。排查思路先确认前端渲染逻辑没问题再回来看后端返回的树结构。我遇到最多的情况是组装树之前没有按parent_id排序子节点先于父节点被处理挂靠时父节点还没创建。解决办法是先对全量菜单排序或者组装时用 Map 暂存所有节点先建 Map 再遍历挂载。上面用的groupingBy方式就没有这个问题因为它天然按父 ID 分组。还有个隐蔽的坑菜单的parent_id为 NULL 而不是 0。建表时一定要把默认值设成 0避免 NULL 混进来否则所有树查询都要加OR IS NULL条件一不留神就出事。6.2 用户改了角色权限没有立即生效现象给用户改了角色或菜单权限之后用户在前端仍然能访问旧权限。根源是权限缓存。登录时把用户权限放进 Redis改权限后没有清理缓存。解决方案有两个一是简单粗暴权限变更后删除该用户或相关角色的缓存 key。但用户和角色的关联要在改动时反查出来再删除对应用户的缓存。二是引入版本号每个用户的权限缓存 key 带版本号比如user:perms:{userId}:{version}。用户表加version字段改权限时版本号加 1前端下次请求用新 key 取权限。这样设计更优雅适合权限变动频繁的项目。我实际使用版本号方案的体会是它还能顺便解决分布式环境下多实例缓存一致性的问题因为只要版本号变了旧 key 自然失效。6.3 用户列表查询越来越慢现象用户表数据量到几十万后列表查询需要好几秒。排查步骤先看慢查询日志问题多半出在关联查询。比如用户列表要显示每个用户的角色名如果逐行查询角色就是典型的 N1 问题。解决方式是用一次关联查询查出角色再在内存里映射或者干脆在用户表冗余一个主角色名字段。另外username、phone这类高频查询字段务必加索引。我在用户表加了(username, status)联合索引后登录查询从 200ms 降到 5ms。索引不是越多越好但高频查询字段绝对不能裸奔。分页深度也别忽视LIMIT 100000, 20这种深分页会越翻越慢解决方案是记录上一页最大 ID 做游标分页或者用覆盖索引延迟关联这些手段在数据量大时非常有效。6.4 并发修改角色菜单关联数据错乱现象两个管理员同时修改同一个角色的菜单权限后提交的覆盖先提交的甚至出现半新半旧的脏关联。本质是丢失更新。方案有两种乐观锁角色表加version字段更新时UPDATE sys_role SET ..., version version 1 WHERE id ? AND version ?影响行数为 0 则说明版本冲突提示用户刷新重试。更彻底的是 Redis 分布式锁在修改角色权限这个动作上加锁String lockKey lock:role:update: roleId; boolean locked redisLock.tryLock(lockKey, 5, TimeUnit.SECONDS); if (!locked) { throw new ServiceException(角色正在被他人修改请稍后重试); } try { // 先删后插角色菜单关联整个过程放在同一个事务里 roleMenuMapper.deleteByRoleId(roleId); roleMenuMapper.batchInsert(roleId, menuIds); } finally { redisLock.unlock(lockKey); }注意先删后插的风险如果删除成功但插入失败这个角色就没有任何权限了。所以两步必须放在同一个事务里且插入要用批量插入而不是循环单条插入。我见过循环插入导致线上接口耗时 3 秒的例子改成batchInsert后直接降到 50ms。7. 系统管理模块的高阶扩展7.1 数据权限从功能权限到行级控制RBAC 管的是能不能进入这个功能但没解决进入之后能看到哪些数据。比如两个销售都能查客户列表一个只能看自己省的数据另一个能看全国。这种数据级权限要在 SQL 层面动态拼接部门或用户维度的过滤条件。实现思路是定义一个数据权限上下文把当前用户的部门范围、数据权限类型放到请求上下文中再通过自定义注解加 AOP 在 mapper 查询前动态拼接条件。RuoYi 里的DataScope注解就是这么做的。最需要注意的是拼接 SQL 的条件必须参数化千万不要用字符串拼接用户输入否则就是 SQL 注入漏洞。数据权限这个主题可以单独写一篇长文这里先点到为止但设计系统管理模块时一定要留出扩展位别把权限逻辑写死在业务代码里。7.2 权限模块独立成服务的取舍当系统管理模块被多个子系统共用时可以考虑把它抽成独立的认证授权服务通过 OpenFeign 或 HTTP 接口对外提供用户和权限查询能力。抽出去之前要想清楚三件事权限缓存的一致性由谁负责子系统是同步拉取还是监听变更事件认证凭证从内网传递到子系统的安全链路怎么设计。这里我只提一个建议没有多个系统共用的明确需求别急着拆微服务。单体应用里的系统管理模块性能最好、开发最顺手拆出去之后网络开销和一致性成本都会上来。这是很多团队为了微服务而微服务踩过的坑。如果你正面临多个 Java 后端项目合并的场景优先考虑在单体里统一权限模型比一上来就搞服务拆分稳妥得多。我个人在实际项目里最深的体会是系统管理模块从来不是写完 CRUD 就完事的模块它的设计质量直接决定了后续每一个业务模块的开发效率。权限模型搭得稳业务功能里就不用天天纠结谁能不能访问日志审计做得全线上出了事故能五分钟定位问题而不是五小时。如果你正在规划新项目我建议把第一周时间老老实实花在这个模块上把 RBAC 表设计、权限缓存、操作日志这三件事想透后面几十个业务模块都会受益。最后再分享一个小技巧。系统管理模块的接口路径我一直沿用 RuoYi 那套/system/user/list、/system/role/list、/system/menu/treeselect的风格。这套命名经大量项目验证语义清晰前端路由和权限标识都能对齐。如果你还没有形成自己的命名规范直接抄它是成本最低的选择。