龙梨丝 发表于 2025-6-9 15:12:14

芋道源码解析之数据权限

文章首发于我的博客:https://blog.liuzijian.com/post/source-code-yudao-data-permission.html
博主和芋道源码作者及其官方开发团队无任何关联
一、引言

芋道的数据权限模块代码,涉及的类和方法很多,环环相扣,需要运行项目一步一步debug分析才能看懂。该模块的代码按照功能细分,大致可以分为两部分:
1.数据权限SQL拦截器:根据定义好的数据权限规则来为涉及到的表在更新、查询和删除时重写(追加)SQL条件,使得用户只能访问到权限范围内的数据。
2.数据权限注解处理器:基于Spring AOP实现,通过自定义一个数据权限注解并实现一个注解处理器来为某些方法单独指定数据权限规则。
两个部分需要配合使用。
二、数据权限SQL拦截器

2.4.0-jdk8-SNAPSHOT版本的数据权限功能是基于mybatis-plus的插件机制实现的,具体是对执行修改、删除和查询的SQL进行拦截、解析,然后再根据数据权限规则对需要限制的表重写(追加)查询条件。使用该插件需要实现MultiDataPermissionHandler接口。
2.1 主要涉及类和接口

2.1.1 Class Diagram


2.1.2 mybatis-plus


[*]com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor 类,数据权限的入口,执行解析和重写逻辑,并加入到mp插件队列中。具体见:DataPermissionInterceptor源码解读
[*]com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor 抽象类,被DataPermissionInterceptor类继承,继承自JsqlParserSupport。提供SQL深度解析能力,遍历SQL语句中各个需要拼接条件的位置,在调用子类来根据不同的表和字段进行SQL重写。详见:BaseMultiTableInnerInterceptor源码解读
[*]com.baomidou.mybatisplus.extension.parser.JsqlParserSupport 抽象类,是mp对jsqlparser的封装,更好的实现SQL的解析。
[*]com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor 接口,被DataPermissionInterceptor类实现,由mp调用,在适当时机触发实现类去执行相关方法,进而使实现类执行SQL解析和重写的功能。
[*]com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler 接口,用于获取数据权限,由实现类来根据不同的表和字段进行SQL重写。
[*]com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler 接口,作用同MultiDataPermissionHandler。
2.1.3 yudao


[*]cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionRuleHandler 类,间接实现DataPermissionHandler接口,根据mp传来的表名和对应查询条件,寻找匹配的数据权限规则来进行数据权限SQL条件的重写,并将符合的多个数据权限策略各自生成的条件进行拼接,返回给mp权限插件。
[*]cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory 接口,数据权限工厂,实现类来根据实际场景对所有适用的数据权限类根据实际情况进行一些筛选或修改,实现在一些特殊场景下改变数据权限的范围,效力以及优先级。
[*]cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl 类,DataPermissionRuleFactory的实现。
[*]cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule 接口,由实现类继承后来实现某种数据权限规则。
[*]cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule 类,yudao项目默认的数据权限,通过实现DataPermissionRule接口实现了部门级别的数据权限规则。
2.2 执行流程源码解读

2.2.1 Sequence Diagram


2.2.2 DataPermissionInterceptor

该类是mybatis-plus数据权限插件的执行入口,是SQL解析和重写功能的起点。
该类在SQL执行前,会对执行的动作进行拦截,并拿到要执行的SQL,递归对SQL语句各处进行扫描,扫描到表和条件时,调用DataPermissionHandler获取当前表的数据权限过滤条件(Expression)对象,再和业务逻辑的查询条件拼在一起,从而实现数据库层面的数据权限控制。
public class DataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {

    private DataPermissionHandler dataPermissionHandler;

    @SuppressWarnings("RedundantThrows")
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
      if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
            return;
      }
      PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
      mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
    }

    @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()));
      }
    }

    ......

    @Override
    public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
      if (dataPermissionHandler == null) {
            return null;
      }
      // 只有新版数据权限处理器才会执行到这里
      final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
      return handler.getSqlSegment(table, where, whereSegment);
    }   

}

解读:

[*]beforeQuery()和beforePrepare()是从接口InnerInterceptor继承来的方法,由mybatis-plus在SQL查询前或者预编译(增删改)前回调并传入要执行的SQL,从而叫该类对即将执行的SQL进行某些操作。两个方法都会调用mpBs.sql(parserXxxx(......))方法对SQL进行解析重写,beforeQuery()调用的是parserSingle(......),只能处理单条SQL。beforePrepare()调用的是parserMulti(......),可以处理多条SQL,因为jdbc能一次执行用分号间隔的多条增删改SQL语句,就需要parserMulti将每次执行的语句分开,如果确实是一次执行多条的情况,就需要逐个进行解析和重写,再将新的拼接在一起,而查询一次只能执行一条,故采用parserSingle即可。
[*]parserSingle()和parserMulti()都是间接继承自抽象类JsqlParserSupport的方法,用于启动多条和单条SQL的递归解析,过程详见:BaseMultiTableInnerInterceptor源码解读,解析获取SQL语句每个部分上的表和对应的条件信息,再调用buildTableExpression()方法,并在方法内再调用handler.getSqlSegment(table, where, whereSegment);,将解析到的表table及条件where和当前执行目标whereSegment传入,向DataPermissionRuleHandler获取当前表的数据权限规则。
[*]为了更好的对要处理的SQL进行改写,beforeQuery()将mybatis的BoundSql boundSql对象转换为mybatis-plus的MPBoundSql mpBs对象。beforePrepare()将mybatis的StatementHandler sh转换为mybatis-plus的MPStatementHandler mpSh对象后再获取mybatis-plus的MPBoundSql mpBs对象。
[*]新增不涉及数据权限,因此beforePrepare()方法中不会针对insert的情况进行处理。
2.2.3 DataPermissionRuleHandler

该类是接口DataPermissionHandler的实现,供拦截器DataPermissionInterceptor调用,用于找到某个表在当前业务下适用的所有的数据权限规则,并汇总,然后再返回一个总的数据权限规则对象给拦截器
@RequiredArgsConstructor
public class DataPermissionRuleHandler implements MultiDataPermissionHandler {

    private final DataPermissionRuleFactory ruleFactory;

    @Override
    public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
      // 获得 Mapper 对应的数据权限的规则
      List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);
      if (CollUtil.isEmpty(rules)) {
            return null;
      }

      // 生成条件
      Expression allExpression = null;
      for (DataPermissionRule rule : rules) {
            // 判断表名是否匹配
            String tableName = MyBatisUtils.getTableName(table);
            if (!rule.getTableNames().contains(tableName)) {
                continue;
            }

            // 单条规则的条件
            Expression oneExpress = rule.getExpression(tableName, table.getAlias());
            if (oneExpress == null) {
                continue;
            }
            // 拼接到 allExpression 中
            allExpression = allExpression == null ? oneExpress
                  : new AndExpression(allExpression, oneExpress);
      }
      return allExpression;
    }

}
解读:

[*]拦截器DataPermissionInterceptor解析到具体的表时会调用该类的getSqlSegment(Table table, Expression where, String mappedStatementId)方法,传入表table,已有条件where和当前执行的目标mappedStatementId,数据权限这里只会用到参数table,没有用到where和mappedStatementId。
[*]List rules = ruleFactory.getDataPermissionRule(mappedStatementId);用于从ruleFactory数据权限规则工厂对象获取所有当前业务下生效了的数据权限,如果没有数据权限规则直接返回null,如果有定义好的数据权限规则对象则进行下一步的匹配,参数mappedStatementId在这个版本的源码中并没有实际用到。
[*]for (DataPermissionRule rule : rules)循环遍历当前生效的所有数据权限规则对象List rules,通过if (!rule.getTableNames().contains(tableName))判断当前表在哪些规则下不需要数据权限进行跳过,没有跳过的都需要进行数据权限条件拼接,如果都跳过了就等于返回null。
[*]最终返回的规则对象是一个总的规则allExpression,如果某个表匹配了多个DataPermissionRule规则,则用AndExpression(allExpression, oneExpress)拼接每个表的规则oneExpress到总的规则allExpression上面,最终allExpression作为当前表的数据权限规则返回。
如果一个表适用多个数据权限规则,则最终的SQL条件之间是and的关系

2.2.4 DataPermissionRuleFactory

数据权限规则"工厂",供DataPermissionRuleHandler调用来获取当前业务下适用的数据权限规则,该类会配合数据权限注解处理器来使用,从线程上下文DataPermissionContextHolder中获取加了@DataPermission数据权限注解且是最近一级调用当前mapper执行SQL的那个业务方法上面的@DataPermission注解,根据注解上的数据权限规则进行匹配,返回当前业务方法下具体适用的数据权限规则,而不是简单的把所有定义好了的数据权限规则都返回。
public interface DataPermissionRuleFactory {

    /**
   * 获得所有数据权限规则数组
   *
   * @return 数据权限规则数组
   */
    List<DataPermissionRule> getDataPermissionRules();

    /**
   * 获得指定 Mapper 的数据权限规则数组
   *
   * @param mappedStatementId 指定 Mapper 的编号
   * @return 数据权限规则数组
   */
    List<DataPermissionRule> getDataPermissionRule(String mappedStatementId);

}@RequiredArgsConstructor
public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {

    /**
   * 数据权限规则数组
   */
    private final List<DataPermissionRule> rules;

    @Override
    public List<DataPermissionRule> getDataPermissionRules() {
      return rules;
    }

    @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
    public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
      // 1. 无数据权限
      if (CollUtil.isEmpty(rules)) {
            return Collections.emptyList();
      }
      // 2. 未配置,则默认开启
      DataPermission dataPermission = DataPermissionContextHolder.get();
      if (dataPermission == null) {
            return rules;
      }
      // 3. 已配置,但禁用
      if (!dataPermission.enable()) {
            return Collections.emptyList();
      }

      // 4. 已配置,只选择部分规则
      if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) {
            return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass()))
                  .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
      }
      // 5. 已配置,只排除部分规则
      if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) {
            return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass()))
                  .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
      }
      // 6. 已配置,全部规则
      return rules;
    }

}解读:

[*]DataPermissionContextHolder.get()从线程上下文获取数据权限注解处理器为当前执行的SQL具体指定的数据权限规则。
2.2.5 DataPermissionRule

DataPermissionRule,数据权限规则接口,用于定义某种数据权限规则,需要通过getTableNames()来声明适用的表,再通过Expression getExpression(String tableName, Alias tableAlias)来定义某个表的数据权限条件
public interface DataPermissionRule {

    /**
   * 返回需要生效的表名数组
   * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据
   *
   * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得
   *
   * @return 表名数组
   */
    Set<String> getTableNames();

    /**
   * 根据表名和别名,生成对应的 WHERE / OR 过滤条件
   *
   * @param tableName 表名
   * @param tableAlias 别名,可能为空
   * @return 过滤条件 Expression 表达式
   */
    Expression getExpression(String tableName, Alias tableAlias);

}DeptDataPermissionRule,yudao自带的一个默认的数据权限规则实现类,可以针对系统中所有的表实现本人、本部门、本部门及以下、指定部门、无任何权限和无任何限制的6种数据权限。需要使用该规则的模块只需要将需要限制数据权限的表和其中对应的字段注册到这个类中,即可实现根据每个用户的数据权限范围对不同的表进行个人和部门级别的数据权限控制,实现这6种权限。
@AllArgsConstructor@Slf4jpublic class DeptDataPermissionRule implements DataPermissionRule {    /**   * LoginUser 的 Context 缓存 Key   */    protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName();    private static final String DEPT_COLUMN_NAME = "dept_id";    private static final String USER_COLUMN_NAME = "user_id";    static final Expression EXPRESSION_NULL = new NullValue();    private final PermissionApi permissionApi;    /**   * 基于部门的表字段配置   * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。   *   * key:表名   * value:字段名   */    private final Map deptColumns = new HashMap();    /**   * 基于用户的表字段配置   * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。   *   * key:表名   * value:字段名   */    private final Map userColumns = new HashMap();    /**   * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集   */    private final Set TABLE_NAMES = new HashSet();    @Override    public Set getTableNames() {      return TABLE_NAMES;    }    @Override    public Expression getExpression(String tableName, Alias tableAlias) {      // 只有有登陆用户的情况下,才进行数据权限的处理      LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();      if (loginUser == null) {            return null;      }      // 只有管理员类型的用户,才进行数据权限的处理      if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {            return null;      }      // 获得数据权限      DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);      // 从上下文中拿不到,则调用逻辑进行获取      if (deptDataPermission == null) {            deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());            if (deptDataPermission == null) {                log.error("", JsonUtils.toJsonString(loginUser));                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",                        loginUser.getId(), tableName, tableAlias.getName()));            }            // 添加到上下文中,避免重复计算            loginUser.setContext(CONTEXT_KEY, deptDataPermission);      }      // 情况一,如果是 ALL 可查看全部,则无需拼接条件      if (deptDataPermission.getAll()) {            return null;      }      // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限      if (CollUtil.isEmpty(deptDataPermission.getDeptIds())            && Boolean.FALSE.equals(deptDataPermission.getSelf())) {            return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空      }      // 情况三,拼接 Dept 和 User 的条件,最后组合      Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());      Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());      if (deptExpression == null && userExpression == null) {            // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据            log.warn("",                  JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));//            throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",//                  loginUser.getId(), tableName, tableAlias.getName()));            return EXPRESSION_NULL;      }      if (deptExpression == null) {            return userExpression;      }      if (userExpression == null) {            return deptExpression;      }      // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)      return new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression));    }    private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) {      // 如果不存在配置,则无需作为条件      String columnName = deptColumns.get(tableName);      if (StrUtil.isEmpty(columnName)) {            return null;      }      // 如果为空,则无条件      if (CollUtil.isEmpty(deptIds)) {            return null;      }      // 拼接条件      return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),                // Parenthesis 的目的,是提供 (1,2,3) 的 () 左右括号                new ParenthesedExpressionList(new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))));    }    private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {      // 如果不查看自己,则无需作为条件      if (Boolean.FALSE.equals(self)) {            return null;      }      String columnName = userColumns.get(tableName);      if (StrUtil.isEmpty(columnName)) {            return null;      }      // 拼接条件      return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId));    }    // ==================== 添加配置 ====================    public void addDeptColumn(Class
页: [1]
查看完整版本: 芋道源码解析之数据权限