ruoyi数据权限@DataPermission源码解析

本文是基于ruoyi-vue-plus5.x版本,仅为学习用,有误的地方欢迎大家批评指正。

权限分类

若依框架权限又分为菜单权限和数据权限

1. 菜单权限

菜单权限,也常被称为功能权限操作权限 ,它控制了用户能够访问系统中的哪些功能模块以及在这些模块中能够执行哪些操作

在若依中,它具体表现为:

  1. 菜单的可见性:左侧的菜单栏中,用户能看到并点击哪些菜单项。
  2. 页面的访问权:用户是否能通过URL直接访问某个功能页面。
  3. 按钮的可用性:在某个页面上,用户能看到并点击哪些按钮(如:新增、修改、删除、导出等)。
实现原理

菜单权限是基于 角色(Role) 来实现的,其逻辑链条如下:

用户 (User) → 角色 (Role) → 菜单/按钮 (Menu/Button)

2. 数据权限

数据权限控制了当用户访问某个功能列表(如用户列表、订单列表)时,能够查看到的数据范围 。它是在用户已经拥有了菜单权限(即能进入这个页面)的前提下,对返回的数据行进行过滤

实现原理

数据权限同样是基于 角色(Role) 来配置的,但其技术实现更为巧妙,主要利用了 AOP(面向切面编程)SQL 动态拼接

技术实现架构

菜单权限我们比较常见,就不着重剖析菜单权限了,今天的主角是数据权限

数据权限核心类/组件表格

类/组件名称 说明 功能
MybatisPlusConfig 数据权限配置 配置 MyBatis-Plus 的全局设置,包括注入数据权限拦截器(PlusDataPermissionInterceptor),确保所有 SQL 查询都能被拦截和处理。
DataScopeType 数据权限模板定义 定义数据权限的模板类型(如 ALL: 全部数据、DEPT: 本部门数据、CUSTOM: 自定义),用于标准化权限过滤逻辑。
DataPermission 数据权限组注解 标注在 Mapper 或 Service 方法上,开启数据权限过滤(默认过滤部门权限)。可指定多个数据列,支持忽略某些角色。
DataColumn 具体的数据权限字段标注 与 DataPermission 配合使用,替换模板中的 key 变量(如指定部门字段 "deptId"),允许自定义 SQL 过滤条件中的字段。
PlusDataPermissionInterceptor 数据权限 SQL 拦截器 拦截所有 SQL 执行,检查方法是否标注了 DataPermission 注解,如果有,则委托处理器添加过滤条件。
PlusDataPermissionHandler 数据权限处理器 处理被拦截的 SQL,根据用户上下文(如部门、角色)动态添加 WHERE 子句,实现权限过滤。
DataPermissionHelper 数据权限助手 操作线程级上下文变量(如设置/获取当前用户的数据权限范围),支持在代码中手动控制权限上下文。
SysDataScopeService 自定义 Bean 处理数据权限 提供自定义扩展接口,用于复杂场景下的数据权限处理(如多租户、角色扩展),可注入到处理器中实现个性化逻辑。

代码拦截流程

sequenceDiagram participant Controller participant Service participant Mapper participant Interceptor as PlusDataPermissionInterceptor participant Handler as PlusDataPermissionHandler participant Helper as DataPermissionHelper participant Custom as SysDataScopeService Controller->>Service: 调用查询方法 (标注 @DataPermission) Service->>Mapper: 调用 Mapper 方法 Mapper->>MyBatis: 执行 SQL MyBatis->>Interceptor: 拦截 beforeQuery Interceptor->>Interceptor: 检查注解 (DataPermission/DataColumn) alt 有注解 Interceptor->>Helper: 获取上下文 (用户权限) Helper-->>Interceptor: 返回 deptId 等 Interceptor->>Handler: 委托 processSql Handler->>DataScopeType: 获取模板类型 DataScopeType-->>Handler: 返回模板 (e.g., DEPT) Handler->>Custom: 调用自定义扩展 Custom-->>Handler: 返回调整后过滤 Handler-->>Interceptor: 返回修改 SQL (添加 WHERE) end Interceptor-->>MyBatis: 更新 BoundSql MyBatis-->>Mapper: 执行过滤后 SQL Mapper-->>Service: 返回结果 Service-->>Controller: 返回响应

核心代码

MybatisPlusConfig

全局启用数据权限拦截,确保所有 Mapper 的 SQL 都能被检查。原理基于 MyBatis-Plus 的插件机制(InnerInterceptor 接口),在 SQL 执行前插入自定义逻辑。

java 复制代码
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
@MapperScan("${mybatis-plus.mapperPackage}")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 数据权限处理
        interceptor.addInnerInterceptor(dataPermissionInterceptor());
        // 分页插件
        interceptor.addInnerInterceptor(paginationInnerInterceptor());
        // 乐观锁插件
        interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
        return interceptor;
    }
 
    //....其他配置
}
DataPermission

这是一个方法级注解,支持配置多个数据权限注解

java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {

    DataColumn[] value();

}
DataColumn

辅助注解,用于 DataPermission 内定义字段映射,一个注解只能对应一个模板。

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataColumn {

    /**
     * 占位符关键字
     */
    String[] key() default "deptName";

    /**
     * 占位符替换值
     */
    String[] value() default "dept_id";

}
PlusDataPermissionInterceptor

实现 MyBatis-Plus 的 InnerInterceptor 接口,重写 beforeQuery 方法,在 SQL 执行前拦截,以及继承JsqlParserSupport实现对sql的解析(依赖JsqlParser库实现)

查询语句数据权限源码

java 复制代码
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 检查忽略注解
        if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
            return;
        }
        // 检查是否无效 无数据权限注解
        if (dataPermissionHandler.isInvalid(ms.getId())) {
            return;
        }
        // 解析 sql 分配对应方法
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
    }

首先是检查是否存在忽略插件的注解@InterceptorIgnore ,mybatis-plus自带的插件忽略,忽略则直接跳过

java 复制代码
 // 内部实现的一个简单内存缓存,如果没有权限注解@DataColumn,下次就直接跳过了,就不会走下面的解析
        if (dataPermissionHandler.isInvalid(ms.getId())) {
            return;
        }
// 这里就是包装了一下,增加了MyBatis反射解析 MetaObject boundSql,
     PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
//反射调用MetaObject boundSql解析后的set方法,替换为增加权限过滤的sql
     mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));

更新和删除数据权限源码

这里就很简单了,解析判断sql语句类型,然后包装反射替换sql

java 复制代码
   @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
                return;
            }
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
        }
    }

具体sql解析替换源码,依赖JsqlParser库解析,然后取得条件where部分,然后增加数据权限过滤sql

java 复制代码
    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody instanceof PlainSelect) {
            this.setWhere((PlainSelect) selectBody, (String) obj);
        } else if (selectBody instanceof SetOperationList) {
            SetOperationList setOperationList = (SetOperationList) selectBody;
            List<SelectBody> selectBodyList = setOperationList.getSelects();
            selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
        }
    }

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
        if (null != sqlSegment) {
            update.setWhere(sqlSegment);
        }
    }

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
        if (null != sqlSegment) {
            delete.setWhere(sqlSegment);
        }
    }

    /**
     * 设置 where 条件
     *
     * @param plainSelect       查询对象
     * @param mappedStatementId 执行方法id
     */
    protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
        Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
        if (null != sqlSegment) {
            plainSelect.setWhere(sqlSegment);
        }
    }
java 复制代码
SelectBody selectBody = select.getSelectBody();

作用:从 Select 对象中提取查询的主体部分(SelectBody)。

原理

  • select 是一个 JSQLParser 的 Select 对象,表示解析后的 SELECT 语句。

  • getSelectBody()

    返回 SELECT 语句的核心部分(SelectBody),它可能是:PlainSelect(普通查询 )、SetOperationList(包含 UNION、INTERSECT 等集合操作的查询)其他类型(如 WithItem,但此处未处理)

处理 SetOperationList

java 复制代码
else if (selectBody instanceof SetOperationList) {
    SetOperationList setOperationList = (SetOperationList) selectBody;
    List<SelectBody> selectBodyList = setOperationList.getSelects();
    selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}

如果 SelectBody 是 SetOperationList 类型(包含 UNION 等操作),提取其子查询列表,并为每个子查询(PlainSelect)添加权限过滤条件。

setWhere 方法

setWhere 方法定义如下(在 PlusDataPermissionInterceptor 中):

java 复制代码
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
    Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
    if (null != sqlSegment) {
        plainSelect.setWhere(sqlSegment);
    }
}

为 PlainSelect 的 WHERE 子句添加权限条件。

  • dataPermissionHandler.getSqlSegment 根据 @DataPermission 注解和用户上下文生成权限条件(Expression 对象,例如 dept_id = 100)。

  • plainSelect.getWhere() 获取当前的 WHERE 子句(可能为 null)。

  • 如果 sqlSegment 不为 null,通过 plainSelect.setWhere 设置新的 WHERE 子句。

  • 如果已有 WHERE 子句,getSqlSegment 可能合并条件(例如 WHERE existing_condition AND dept_id = 100)。

PlusDataPermissionHandler

核心处理器,基于用户上下文构建过滤条件

java 复制代码
 public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
      //先查找是否存在数据权限注解,如果没有就加到无需数据权限校验的缓存方法,也就是上面拦截器中的第二个判断条件   
     DataColumn[] dataColumns = findAnnotation(mappedStatementId);
        if (ArrayUtil.isEmpty(dataColumns)) {
            invalidCacheSet.add(mappedStatementId);
            return where;
        }
     // 拿到当前用户的登录信息
        LoginUser currentUser = DataPermissionHelper.getVariable("user");
        if (ObjectUtil.isNull(currentUser)) {
            currentUser = LoginHelper.getLoginUser();
            DataPermissionHelper.setVariable("user", currentUser);
        }
        // 如果是超级管理员,则不过滤数据
        if (LoginHelper.isAdmin()) {
            return where;
        }
       //真正的按照权限构建sql语句
        String dataFilterSql = buildDataFilter(dataColumns, isSelect);
        if (StringUtils.isBlank(dataFilterSql)) {
            return where;
        }
        try {
            Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
            // 数据权限sql,数据权限使用单独的括号 防止与其他条件冲突
            Parenthesis parenthesis = new Parenthesis(expression);
            if (ObjectUtil.isNotNull(where)) {
                return new AndExpression(where, parenthesis);
            } else {
                return parenthesis;
            }
        } catch (JSQLParserException e) {
            throw new ServiceException("数据权限解析异常 => " + e.getMessage());
        }
    }

过滤sql代码

java 复制代码
  /**
     * 构造数据过滤sql
     */
    private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
        // 更新或删除需满足所有条件
        String joinStr = isSelect ? " OR " : " AND ";
        //这里将用户的信息设置到spel的上下文中,这样在DataScopeType数据权限的模板sql中调用方法时就可以从这个user变量取得对应的角色,从而调用数据权限查询的bean的方法,返回权限 IN (a,b,c)这个集合
        LoginUser user = DataPermissionHelper.getVariable("user");
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setBeanResolver(beanResolver);
        DataPermissionHelper.getContext().forEach(context::setVariable);
        Set<String> conditions = new HashSet<>();
        for (RoleDTO role : user.getRoles()) {
            //设置角色id,用于DataScopeType数据权限的模板sql调用,如#{@sdss.getRoleCustom( #user.roleId )} 
            user.setRoleId(role.getRoleId());
            // 获取角色权限泛型
            DataScopeType type = DataScopeType.findCode(role.getDataScope());
            if (ObjectUtil.isNull(type)) {
                throw new ServiceException("角色数据范围异常 => " + role.getDataScope());
            }
            // 全部数据权限直接返回
            if (type == DataScopeType.ALL) {
                return "";
            }
            boolean isSuccess = false;
            for (DataColumn dataColumn : dataColumns) {
                if (dataColumn.key().length != dataColumn.value().length) {
                    throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
                }
                // 不包含 key 变量 则不处理
                if (!StringUtils.containsAny(type.getSqlTemplate(),
                    Arrays.stream(dataColumn.key()).map(key -> "#" + key).toArray(String[]::new)
                )) {
                    continue;
                }
                // 设置注解变量 key 为表达式变量 value 为变量值
                for (int i = 0; i < dataColumn.key().length; i++) {
                    context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);
                }

                // 解析sql模板并填充
                String sql = parser.parseExpression(type.getSqlTemplate(), parserContext).getValue(context, String.class);
                conditions.add(joinStr + sql);
                isSuccess = true;
            }
            // 未处理成功则填充兜底方案
            if (!isSuccess && StringUtils.isNotBlank(type.getElseSql())) {
                conditions.add(joinStr + type.getElseSql());
            }
        }

        //这里汇总了多个角色的时候的数据权限
        if (CollUtil.isNotEmpty(conditions)) {
            //多个条件拼接为一个:如查询的 OR deptId=xx OR userId=xx
            String sql = StreamUtils.join(conditions, Function.identity(), "");
            //去掉首个条件标识变成:deptId=xx OR userId=xx
            return sql.substring(joinStr.length());
        }
        return "";
    }
DataPermissionHelper

使用 ThreadLocal 存储上下文,如当前的用户信息等

java 复制代码
        //如存储当前用户信息
        LoginUser currentUser = DataPermissionHelper.getVariable("user");
        if (ObjectUtil.isNull(currentUser)) {
            currentUser = LoginHelper.getLoginUser();
            DataPermissionHelper.setVariable("user", currentUser);
        }

       //构建过滤sql
       //取得当前用户的西悉尼,然后添加到spel解析器中,用于后续的数据权限模板中替换变量
     private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
        // 更新或删除需满足所有条件
        String joinStr = isSelect ? " OR " : " AND ";
        LoginUser user = DataPermissionHelper.getVariable("user");
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setBeanResolver(beanResolver);
        DataPermissionHelper.getContext().forEach(context::setVariable);
        Set<String> conditions = new HashSet<>();
        for (RoleDTO role : user.getRoles()) {
            user.setRoleId(role.getRoleId());
DataScopeType

这是一个枚举类,定义权限模板常量。

java 复制代码
@Getter
@AllArgsConstructor
public enum DataScopeType {

    /**
     * 全部数据权限
     */
    ALL("1", "", ""),

    /**
     * 自定数据权限
     */
    CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", ""),

    /**
     * 部门数据权限
     */
    DEPT("3", " #{#deptName} = #{#user.deptId} ", ""),

    /**
     * 部门及以下数据权限
     */
    DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", ""),

    /**
     * 仅本人数据权限
     */
    SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 "),
    /**
     * 销售主体权限
     */
    SALE("6", " #{#saleName} = #{#user.deptId} ", " 1 = 0 ")
    ;

    private final String code;

    /**
     * 语法 采用 spel 模板表达式
     */
    private final String sqlTemplate;

    /**
     * 不满足 sqlTemplate 则填充
     */
    private final String elseSql;

    public static DataScopeType findCode(String code) {
        if (StringUtils.isBlank(code)) {
            return null;
        }
        for (DataScopeType type : values()) {
            if (type.getCode().equals(code)) {
                return type;
            }
        }
        return null;
    }
}
  • code:作为权限的标识符,角色表存在对应的字段记录对应的数据权限

  • sqlTemplate:需要替换的sql模板,使用的是spel解析,比如:#{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) 其中deptName是数据注解@DataColumn的key,会将key替换为value,而value对应的是这次sql查询的某个字段,

    java 复制代码
      @DataPermission({
            @DataColumn(key = "deptName", value = "bab.duty_subject"),
            @DataColumn(key = "userName", value = "bab.ar_user_id")
        })

    如bab.duty_subject表的这个字段需要做数据权限,则实际会替换为

    select xxx from xxx as bab where bab.duty_subject in (xxx),sql模板中的 #{@sdss.getRoleCustom( #user.roleId )}其实是调用了一个数据权限Bean SysDataScopeService的方法,sdss为bean的名称。

    java 复制代码
    @RequiredArgsConstructor
    @Service("sdss")
    public class SysDataScopeServiceImpl implements ISysDataScopeService {
    
        private final SysRoleDeptMapper roleDeptMapper;
        private final SysDeptMapper deptMapper;
    
        @Override
        public String getRoleCustom(Long roleId) {
            ....权限实现
        }
        
        //其他权限方法
    }    
核心代码调用时序图
sequenceDiagram participant MyBatis participant Interceptor as PlusDataPermissionInterceptor participant JsqlParserSupport participant Handler as PlusDataPermissionHandler participant Helper as DataPermissionHelper MyBatis->>Interceptor: beforeQuery(ms, boundSql) Interceptor->>Helper: 检查忽略注解 (InterceptorIgnoreHelper) Helper-->>Interceptor: 返回是否忽略 alt 不忽略 Interceptor->>Handler: 检查注解有效性 (isInvalid) Handler-->>Interceptor: 返回有效 Interceptor->>JsqlParserSupport: parserSingle(sql, ms.getId()) JsqlParserSupport->>JSQLParser: CCJSqlParserUtil.parse(sql) JSQLParser-->>JsqlParserSupport: 返回 Statement JsqlParserSupport->>Interceptor: processParser(statement, 0, sql, ms.getId()) alt SELECT Interceptor->>Interceptor: processSelect(select, 0, sql, ms.getId()) Interceptor->>Handler: getSqlSegment(where, ms.getId()) Handler-->>Interceptor: 返回 Expression (如 dept_id = 100) Interceptor->>Select: setWhere(expression) else UPDATE/DELETE Interceptor->>Handler: getSqlSegment(where, ms.getId()) Handler-->>Interceptor: 返回 Expression Interceptor->>Update/Delete: setWhere(expression) end JsqlParserSupport-->>Interceptor: 返回修改后 SQL Interceptor->>MyBatis: 更新 BoundSql end MyBatis->>Database: 执行 SQL

总结

通过这套机制,若依实现了与业务逻辑的解耦,基于MyBatis-Plus的数据权限框架在设计上更加现代化和灵活,通过模板化的配置和多层次的扩展点,可自动应用复杂的数据权限逻辑,极大提高了开发效率和代码的可维护性。

相关推荐
2501_915921432 分钟前
前端开发工具有哪些?常用前端开发工具、前端调试工具、前端构建工具与效率提升工具对比与最佳实践
android·前端·ios·小程序·uni-app·iphone·webview
知否技术14 分钟前
别再踩坑了!这份 Vue3+TypeScript 项目教程,赶紧收藏!
前端·typescript
IT_陈寒18 分钟前
JavaScript 2024:10个颠覆你认知的ES新特性实战解析
前端·人工智能·后端
meng半颗糖27 分钟前
JavaScript 性能优化实战指南
前端·javascript·servlet·性能优化
EndingCoder29 分钟前
离线应用开发:Service Worker 与缓存
前端·javascript·缓存·性能优化·electron·前端框架
遗憾随她而去.41 分钟前
css3的 --自定义属性, 变量
前端·css·css3
haogexiaole3 小时前
vue知识点总结
前端·javascript·vue.js
哆啦A梦15885 小时前
[前台小程序] 01 项目初始化
前端·vue.js·uni-app
小周同学@7 小时前
谈谈对this的理解
开发语言·前端·javascript
Wiktok7 小时前
Pyside6加载本地html文件并实现与Javascript进行通信
前端·javascript·html·pyside6