在企业级管理系统的 RBAC 权限体系中,机构、人员天然携带机构归属字段,可通过「本机构及下级」规则实现行级数据隔离。但角色作为权限集合的抽象实体,通常设计为全局属性,不绑定机构维度,导致数据权限拦截机制对角色表失效,进而引发两类典型越权问题:
- 纵向越权:低层级管理员可查看、分配甚至编辑平台管理员等高权限角色
- 横向越权:同级别的不同机构管理员,可互相查看、操作对方的角色
针对这个问题,我们可以采用循序渐进的两级方案,从轻量快速修复到完整闭环控制,按需落地,兼顾开发成本、使用体验与系统安全性。
一、基础版:角色权重等级制(纵向隔离)
适合角色体系扁平、需要快速解决核心越权问题的场景,改动量极小,落地成本极低,无需改造现有机构权限体系。
1. 核心设计思路
给每个角色赋予一个整数权重等级,数值越小代表权限层级越高。通过对比「当前用户自身的最高角色等级」与「目标角色的等级」,实现纵向的权限边界控制,从数据层面天然隔绝高权限角色对低权限用户的可见性,彻底解决上下层级越权问题。
2. 表结构改造
仅需在角色表
sys_role 中新增 1 个字段,对原有业务逻辑无侵入、无兼容问题:role_level:int 类型,角色权限等级,数值越小,权限层级越高参考等级定义规范:
角色名称 | role_level 值 | 权限层级说明 |
|---|---|---|
平台管理员 | 1 | 系统最高权限 |
系统管理员 | 2 | 平台级运维管理 |
机构管理员 | 5 | 单机构管理权限 |
普通用户 | 10 | 基础业务权限 |
3. 核心权限规则(后端强校验)
所有规则均以服务端强制校验为准,前端仅做展示适配、优化交互,禁止仅靠前端隐藏按钮控制权限,防止抓包越权。
(1)查看与分配权限
规则:当前用户仅可查看、分配
role_level >= 自身最高角色等级 的角色逻辑解读:只能看到权限等级等于或低于自己的角色,高等级角色对低权限用户完全不可见,从列表根源杜绝纵向越权。
场景示例:机构管理员(level=5)只能看到等级为 5、10 的角色,平台管理员、系统管理员等高权限角色自动隐藏。
(2)编辑与删除权限
规则:当前用户仅可编辑、删除
role_level > 自身最高角色等级 的角色逻辑解读:只能操作权限严格低于自己的角色;同级角色仅可查看、分配,不可修改删除,完美契合「能看到自身角色但不能编辑」的业务诉求。
场景示例:机构管理员(level=5)只能编辑普通用户角色(level=10),无法编辑同级机构管理员角色、高等级平台角色。
4. 方案优缺点
优点:实现成本极低,逻辑直观易理解,一套规则覆盖角色查询、账户角色分配、角色编辑删除全链路场景,零业务侵入。
缺点:仅支持纵向等级隔离,无法实现横向跨机构隔离,同级别不同机构管理员可互相查看对方机构的角色。
5. 适用场景
角色体系扁平、单租户单机构、无复杂跨机构隔离需求的中小型系统;适合快速修复核心纵向越权问题,快速落地上线。
二、增强版:权重等级 + 可见机构范围(二维权限控制)
在基础版纵向隔离的基础上,补充「角色可见机构范围」维度,形成 「纵向等级 + 横向机构」的二维控制体系,同时解决纵向越权与横向越权问题,完全复用现有机构权限体系,落地成本可控、扩展性更强。
1. 核心设计思路
保留权重等级的纵向层级管控能力,通过「角色-可见机构」的关联关系,限定角色的生效与可见范围。用户只能查看、分配、操作同时满足「等级不越权」和「机构在自身管辖范围内」的角色,彻底实现跨机构横向隔离,杜绝同级跨机构越权。
2. 表结构设计
(1)角色表保留权重字段
沿用
sys_role 表的 role_level 字段,负责纵向等级控制,完全兼容基础版逻辑,无需改动原有数据。(2)新增角色-机构可见关联表
新建
sys_role_org 中间表,存储每个角色的可见机构范围,一对多关联,支持角色多机构共享:字段名 | 类型 | 说明 |
|---|---|---|
id | bigint | 主键 |
role_id | bigint | 角色 ID |
org_id | bigint | 可见机构 ID |
补充说明:若系统角色数量极少,可直接在
sys_role 表增加 visible_org_ids 字段以逗号分隔存储机构ID;中间表方案更规范、性能更好,可直接复用现有数据权限 SQL 拦截逻辑,长期维护性更优。3. 核心双重校验规则
所有权限判断必须同时满足等级约束 + 机构约束,二者为「且」关系,全接口后端强制生效,无任何绕过入口。
(1)角色可见/可分配规则(列表查询、账户分配下拉)
用户能看到、能分配给他人的角色,必须同时满足两个条件:
- 纵向约束:目标角色的
role_level >= 当前用户自身最高角色等级,杜绝高低层级越权 - 横向约束:目标角色的可见机构集合,与当前用户的数据权限机构集合(本机构及下级)存在交集,杜绝同级跨机构越权
SQL 过滤逻辑伪代码:
WHERE
-- 1. 等级不越权
r.role_level >= #{当前用户最高角色等级}
-- 2. 机构范围有交集
AND EXISTS (
SELECT 1 FROM sys_role_org ro
WHERE ro.role_id = r.id
AND ro.org_id IN (#{当前用户数据权限内的机构ID集合})
)(2)角色编辑/删除规则
用户能修改、删除的角色,校验规则更严格,同时满足:
- 纵向约束:目标角色的
role_level > 当前用户自身最高角色等级,同级角色仅可查看、不可编辑 - 横向约束:目标角色的全部可见机构,都包含在当前用户的数据权限机构集合内
该规则可避免「角色共享给多个机构,被单个机构管理员私自修改」的问题,核心逻辑:仅角色创建机构及上级有权编辑,其他共享机构仅拥有使用权。
4. 新增角色的交互边界
完全复用系统现有机构选择组件,交互体验与机构管理、人员管理功能保持一致,用户零学习成本:
- 可选范围限制:机构选择器仅展示当前用户权限内的本机构及下级机构,禁止越权选择上级、外机构
- 默认值优化:默认勾选当前用户所属机构,支持「选中机构自动包含所有下级」,与系统现有数据权限规则对齐
- 等级约束:新增角色的可选等级下限为用户自身等级,前端隐藏所有高等级选项,从源头规避越权创建
- 平台级特殊处理:平台管理员可创建「全平台可见」的通用角色,适配平台统一运维场景
5. 方案核心优势
- 安全闭环完整:同时解决纵向、横向两类越权问题,低等级看不到高等级、跨机构看不到同级角色
- 灵活度更高:支持角色跨机构共享,无需每个机构重复创建相同角色,减少冗余数据
- 复用性强:机构树、数据权限计算、拦截器逻辑均可复用现有体系,开发成本可控
- 平滑升级:存量角色可批量初始化可见机构数据,无需停机改造,业务无感知
6. 局限性
角色与机构数量极大时,需优化中间表索引与查询逻辑,整体性能可控;不适合超大型多租户强隔离、角色体系极其复杂的系统,此类场景推荐完整的角色机构化归属方案。
三、方案对比与落地建议
1. 方案对比总览
方案 | 隔离维度 | 实现成本 | 灵活性 | 核心解决问题 |
|---|---|---|---|---|
纯权重等级制 | 仅纵向 | 极低 | 一般 | 快速修复纵向越权 |
权重 + 可见机构 | 纵向 + 横向 | 中等 | 高 | 完整解决纵横两类越权 |
2. 分步落地建议
- 第一步(紧急止血):落地纯权重等级制,新增字段并改造列表、分配接口,快速解决核心纵向越权问题
- 第二步(完善闭环):补充可见机构中间表与横向校验逻辑,实现跨机构隔离,完成权限体系闭环
- 第三步(体验优化):增加角色标签、不可操作项置灰、分组展示等前端优化,降低用户认知成本
3. 通用安全兜底
无论采用哪套方案,以下安全原则必须严格遵守:
- 全接口后端强校验,禁止仅靠前端控制权限
- 系统内置最高级管理员账号豁免所有校验,避免权限配置错误导致系统锁死
- 禁止用户编辑、删除自己当前正在使用的角色,防止误操作锁死或违规提权
- 所有角色的增删改、分配操作全量留痕,支持安全审计追溯
四、总结
角色权重等级制是轻量解决纵向越权的最优解,在此基础上扩展可见机构维度,即可用中等成本实现接近完整机构化方案的权限控制效果,是绝大多数多层级机构管理系统的高性价比选择。方案整体可平滑演进,无技术债务,可随业务规模逐步升级。
附录:基于 MyBatis-Plus 的数据权限拦截器实现示例
以下代码基于 MyBatis-Plus 3.5.x 版本的 InnerInterceptor 扩展机制实现,采用注解 + 拦截器的方式,对业务代码零侵入,可直接复用现有数据权限架构。
一、基础准备:用户权限上下文
先定义统一的用户上下文,存放当前登录用户的最高角色等级与数据权限机构集合。
/**
* 当前登录用户权限上下文
*/
@Data
public class LoginUserContext {
/** 当前用户ID */
private Long userId;
/** 当前用户所属机构ID */
private Long orgId;
/** 当前用户自身最高角色等级(数值越小等级越高) */
private Integer maxRoleLevel;
/** 当前用户数据权限范围内的机构ID集合(本机构+所有下级) */
private List<Long> dataScopeOrgIds;
/** 是否超级管理员(豁免所有校验) */
private Boolean isSuperAdmin;
// 从ThreadLocal中获取当前用户
public static LoginUserContext current() {
// 实际项目中从Security上下文或Token解析中获取
return UserContextHolder.get();
}
}二、基础版:纯角色等级拦截器
对应「纯权重等级制」方案,仅做纵向等级过滤。
1. 自定义注解
/**
* 角色等级数据权限注解
* 标记在Mapper方法上,自动注入角色等级过滤条件
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleLevelDataScope {
/**
* 角色等级字段名,默认 role_level
*/
String levelField() default "role_level";
}2. 拦截器实现
/**
* 角色等级数据权限拦截器
* 自动为角色查询SQL追加等级过滤条件,防止纵向越权
*/
@Component
@Slf4j
public class RoleLevelDataScopeInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// 1. 获取方法上的注解,没有则跳过
RoleLevelDataScope annotation = getDataScopeAnnotation(ms);
if (annotation == null) {
return;
}
// 2. 获取当前用户,超管直接放行
LoginUserContext user = LoginUserContext.current();
if (user == null || Boolean.TRUE.equals(user.getIsSuperAdmin())) {
return;
}
// 3. 拼接过滤SQL
String originalSql = boundSql.getSql();
String levelField = annotation.levelField();
// 规则:只能查看 role_level >= 自身最高等级 的角色
String scopeSql = String.format("%s >= %d", levelField, user.getMaxRoleLevel());
// 4. 注入到原SQL中
String newSql = buildWhereSql(originalSql, scopeSql);
// 反射修改BoundSql中的SQL
ReflectionUtils.setFieldValue(boundSql, "sql", newSql);
}
/**
* 在WHERE后追加条件,兼容已有WHERE和无WHERE的情况
*/
private String buildWhereSql(String originalSql, String scopeCondition) {
// 简化实现,实际项目建议用JSqlParser解析SQL,避免字符串拼接风险
if (originalSql.toUpperCase().contains("WHERE")) {
return originalSql.replaceFirst("(?i)WHERE", "WHERE " + scopeCondition + " AND ");
} else {
// 在ORDER BY / LIMIT 前插入WHERE
return originalSql.replaceFirst("(?i)(ORDER BY|LIMIT|$)", " WHERE " + scopeCondition + " $1");
}
}
/**
* 从MappedStatement中获取注解
*/
private RoleLevelDataScope getDataScopeAnnotation(MappedStatement ms) {
try {
String id = ms.getId();
String className = id.substring(0, id.lastIndexOf('.'));
String methodName = id.substring(id.lastIndexOf('.') + 1);
Class<?> mapperClass = Class.forName(className);
Method[] methods = mapperClass.getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)
&& method.isAnnotationPresent(RoleLevelDataScope.class)) {
return method.getAnnotation(RoleLevelDataScope.class);
}
}
} catch (Exception e) {
log.warn("解析角色等级注解失败", e);
}
return null;
}
}3. Mapper 使用方式
public interface SysRoleMapper extends BaseMapper<SysRole> {
/**
* 查询角色列表(自动注入等级权限过滤)
*/
@RoleLevelDataScope
List<SysRole> selectRoleList(RoleQuery query);
/**
* 账户分配时查询可选角色(复用同一套过滤规则)
*/
@RoleLevelDataScope
List<SysRole> selectAssignableRoleList(Long orgId);
}编辑、删除接口的等级校验建议放在 Service 层单独做判断,不通过拦截器实现,逻辑更清晰:
// 编辑前校验
if (role.getRoleLevel() <= currentUser.getMaxRoleLevel()) {
throw new BizException("无权编辑该等级的角色");
}三、增强版:等级 + 机构双重拦截器
对应「权重 + 可见机构」方案,在等级过滤基础上追加机构范围过滤。
1. 自定义注解升级
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleDataScope {
/** 角色等级字段名 */
String levelField() default "r.role_level";
/** 角色表别名 */
String roleAlias() default "r";
/**
* 校验模式
* VIEW:可见性校验(有交集即可),用于列表查询、分配下拉
* EDIT:编辑性校验(全部包含),用于编辑删除
*/
ScopeMode mode() default ScopeMode.VIEW;
}
public enum ScopeMode {
VIEW, EDIT
}2. 拦截器实现
/**
* 角色二维数据权限拦截器
* 同时校验:角色等级 + 可见机构范围
*/
@Component
@Slf4j
public class RoleDataScopeInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
RoleDataScope annotation = getDataScopeAnnotation(ms);
if (annotation == null) {
return;
}
LoginUserContext user = LoginUserContext.current();
if (user == null || Boolean.TRUE.equals(user.getIsSuperAdmin())) {
return;
}
String originalSql = boundSql.getSql();
StringBuilder condition = new StringBuilder();
// 1. 纵向等级过滤
condition.append(String.format("%s >= %d",
annotation.levelField(), user.getMaxRoleLevel()));
// 2. 横向机构过滤(VIEW模式:存在交集即可)
if (annotation.mode() == ScopeMode.VIEW) {
String orgIds = user.getDataScopeOrgIds().stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
condition.append(" AND EXISTS (")
.append("SELECT 1 FROM sys_role_org ro ")
.append("WHERE ro.role_id = ").append(annotation.roleAlias()).append(".id ")
.append("AND ro.org_id IN (").append(orgIds).append(")")
.append(")");
}
// 注入SQL
String newSql = buildWhereSql(originalSql, condition.toString());
ReflectionUtils.setFieldValue(boundSql, "sql", newSql);
}
// buildWhereSql、getDataScopeAnnotation 方法同上,略
}3. 编辑操作的机构校验(Service 层)
编辑/删除场景要求「角色的全部可见机构都在用户权限范围内」,在 Service 层显式校验:
@Service
public class SysRoleService extends ServiceImpl<SysRoleMapper, SysRole> {
@Resource
private SysRoleOrgMapper roleOrgMapper;
public void updateRole(SysRole role) {
LoginUserContext user = LoginUserContext.current();
// 1. 等级校验
if (role.getRoleLevel() <= user.getMaxRoleLevel()) {
throw new BizException("无权编辑该等级的角色");
}
// 2. 机构范围校验:角色所有可见机构必须都在用户数据权限内
List<Long> roleOrgIds = roleOrgMapper.selectOrgIdsByRoleId(role.getId());
if (!user.getDataScopeOrgIds().containsAll(roleOrgIds)) {
throw new BizException("该角色包含超出您管辖范围的机构,无权编辑");
}
// 3. 执行更新
this.updateById(role);
}
}四、使用与优化建议
1. SQL 解析建议
生产环境推荐使用 JSqlParser 解析 SQL 语法树来追加条件,替代字符串拼接方式,可完美兼容复杂联表查询、子查询等场景,避免字符串替换出错。
2. 性能优化
sys_role_org表建议建立联合索引:idx_role_org(role_id, org_id)- 用户的机构集合
dataScopeOrgIds建议在登录时一次性计算并缓存,避免每次查询都递归查机构树
3. 拦截器配置方式
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 角色数据权限拦截器,放在分页拦截器之前
interceptor.addInnerInterceptor(new RoleDataScopeInterceptor());
// 分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}4. 扩展思路
可在此基础上继续扩展「角色分配时的等级二次校验」「自身角色保护」等规则,形成完整的安全校验体系。
