个人随笔
目录
角色管理越权问题的两级解决方案:从权重等级到二维权限控制
2026-06-24 11:09:29
在企业级管理系统的 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. 扩展思路

可在此基础上继续扩展「角色分配时的等级二次校验」「自身角色保护」等规则,形成完整的安全校验体系。
 4

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号-2