芋道源码解析之数据权限

本文最后更新于 2025年2月26日

博主和芋道源码作者及其官方开发团队无任何关联

一、引言

芋道的数据权限模块代码,涉及的类和方法很多,环环相扣,需要运行项目一步一步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插件队列中。

  • 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);
    }    

}

解读:

  1. beforeQuery()beforePrepare()是从接口InnerInterceptor继承来的方法,由mybatis-plus在SQL查询前或者预编译(增删改)前回调并传入要执行的SQL,从而叫该类对即将执行的SQL进行某些操作。两个方法都会调用mpBs.sql(parserXxxx(......))方法对SQL进行解析重写,beforeQuery()调用的是parserSingle(......),只能处理单条SQL。beforePrepare()调用的是parserMulti(......),可以处理多条SQL,因为jdbc能一次执行用分号间隔的多条增删改SQL语句,就需要parserMulti将每次执行的语句分开,如果确实是一次执行多条的情况,就需要逐个进行解析和重写,再将新的拼接在一起,而查询一次只能执行一条,故采用parserSingle即可。

  2. parserSingle()parserMulti()都是间接继承自抽象类JsqlParserSupport的方法,用于启动多条和单条SQL的递归解析,过程详见:BaseMultiTableInnerInterceptor源码解读,解析获取SQL语句每个部分上的表和对应的条件信息,再调用buildTableExpression()方法,并在方法内再调用handler.getSqlSegment(table, where, whereSegment);,将解析到的表table及条件where和当前执行目标whereSegment传入,向DataPermissionRuleHandler获取当前表的数据权限规则。

  3. 为了更好的对要处理的SQL进行改写,beforeQuery()将mybatis的BoundSql boundSql对象转换为mybatis-plus的MPBoundSql mpBs对象。beforePrepare()将mybatis的StatementHandler sh转换为mybatis-plus的MPStatementHandler mpSh对象后再获取mybatis-plus的MPBoundSql mpBs对象。

  4. 新增不涉及数据权限,因此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;
    }

}

解读:

  1. 拦截器DataPermissionInterceptor解析到具体的表时会调用该类的getSqlSegment(Table table, Expression where, String mappedStatementId)方法,传入表table,已有条件where和当前执行的目标mappedStatementId,数据权限这里只会用到参数table,没有用到where和mappedStatementId。

  2. List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(mappedStatementId);用于从ruleFactory数据权限规则工厂对象获取所有当前业务下生效了的数据权限,如果没有数据权限规则直接返回null,如果有定义好的数据权限规则对象则进行下一步的匹配,参数mappedStatementId在这个版本的源码中并没有实际用到。

  3. for (DataPermissionRule rule : rules)循环遍历当前生效的所有数据权限规则对象List<DataPermissionRule> rules,通过if (!rule.getTableNames().contains(tableName))判断当前表在哪些规则下不需要数据权限进行跳过,没有跳过的都需要进行数据权限条件拼接,如果都跳过了就等于返回null

  4. 最终返回的规则对象是一个总的规则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;
    }

}

解读:

  1. 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
@Slf4j
public 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<String, String> deptColumns = new HashMap<>();
    /**
     * 基于用户的表字段配置
     * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
     *
     * key:表名
     * value:字段名
     */
    private final Map<String, String> userColumns = new HashMap<>();
    /**
     * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集
     */
    private final Set<String> TABLE_NAMES = new HashSet<>();

    @Override
    public Set<String> 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("[getExpression][LoginUser({}) 获取数据权限为 null]", 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("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
                    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<Long> 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<LongValue>(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<? extends BaseDO> entityClass) {
        addDeptColumn(entityClass, DEPT_COLUMN_NAME);
    }

    public void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName) {
        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
       addDeptColumn(tableName, columnName);
    }

    public void addDeptColumn(String tableName, String columnName) {
        deptColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }

    public void addUserColumn(Class<? extends BaseDO> entityClass) {
        addUserColumn(entityClass, USER_COLUMN_NAME);
    }

    public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) {
        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
        addUserColumn(tableName, columnName);
    }

    public void addUserColumn(String tableName, String columnName) {
        userColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }

}

解读:

  1. Map<String, String> deptColumns将需要部门数据权限约束的表的表名和部门ID的字段名通过键值对关联起来,Map<String, String> userColumns则是将需要本人数据权限约束的表的表名和用户ID的字段名通过键值对关联起来,用于之后对不同的表和字段拼接条件。Set<String> TABLE_NAMES则是将适用本类规则的所有表的表名都保存进去,供DataPermissionRuleHandler判断当前解析到的某个表是否匹配本类的数据权限规则。业务模块需要将模块中用到该规则的表名和对应字段名注册到这些集合中。

  2. addDeptColumn(String tableName, String columnName)addUserColumn(String tableName, String columnName)方法以及它们的重载方法,会在该类创建时被各业务模块的配置类DataPermissionConfiguration调用,将每个模块需要用到该规则类的表和对应字段名注册到该类中。

  3. getExpression(String tableName, Alias tableAlias)方法中拼接表的数据权限SQL条件,首先要通过permissionApi.getDeptDataPermission(loginUser.getId())获取当前登录用户的数据权限范围,如果是无限制,直接返回null表示没有规则限制,无论哪个表都查出所有数据。如果既不能查看自己的数据又不能访问任何部门的数据,说明无论哪个表都没有数据权限,直接返回WHERE null = null,执行结果就是空集。剩下的情况就需要根据具体表和字段来返回具体的数据权限条件了,会先后调用buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds)buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId)方法来对当前表拼接部门或本人,或同时拼接部门和本人的数据权限条件,返回对应的Expression对象,找不到表或无权限时返回null,两个方法至少返回一个非空的对象,部门为null则返回本人,本人为null则返回部门,两者都不为null则用OR拼接返回new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression)),既然当前表适用该类定义的规则且当前用户是有权限的,部门和本人的条件便不能同时为null,如出现同时为null的情况则返回new NullValue(),SQL执行会返回空集。

2.2.6 DeptDataPermissionRuleCustomizer

个人及部门级别数据权限的表和字段回调接口,由业务模块实现,将DeptDataPermissionRule对象传入回调方法customize,供各业务模块将需要数据权限控制的表和字段信息注册到DeptDataPermissionRule中。

@FunctionalInterface
public interface DeptDataPermissionRuleCustomizer {

    /**
     * 自定义该权限规则
     * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则
     * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则
     *
     * @param rule 权限规则
     */
    void customize(DeptDataPermissionRule rule);

}

2.2.7 YudaoDeptDataPermissionAutoConfiguration

个人及部门级别数据权限规则的配置类,用于注册DeptDataPermissionRule到Spring容器中,每个模块实现的DeptDataPermissionRuleCustomizer接口实例也会被注入到List<DeptDataPermissionRuleCustomizer> customizers中,遍历调用customize(rule)方法后,所有业务模块配置的本人和部门数据权限相关的表和字段信息就全部注册到了DeptDataPermissionRule对象中。

@AutoConfiguration
@ConditionalOnClass(LoginUser.class)
@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
public class YudaoDeptDataPermissionAutoConfiguration {

    @Bean
    public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
                                                         List<DeptDataPermissionRuleCustomizer> customizers) {
        // 创建 DeptDataPermissionRule 对象
        DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
        // 补全表配置
        customizers.forEach(customizer -> customizer.customize(rule));
        return rule;
    }

}

2.2.8 DataPermissionConfiguration

system业务模块的数据权限配置类,位于cn.iocoder.yudao.module.system.framework.datapermission.config.DataPermissionConfiguration下,用于实例化DeptDataPermissionRuleCustomizer的对象将system模块下需要DeptDataPermissionRule规则限制的表的表名和字段名进行注册。

每一个需要使用DeptDataPermissionRule规则的业务模块(biz)都可以通过创建配置类返回DeptDataPermissionRuleCustomizer的方式实现部门和个人级别的数据权限控制。

@Configuration(proxyBeanMethods = false)
public class DataPermissionConfiguration {

    @Bean
    public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() {
        return rule -> {
            // dept
            rule.addDeptColumn(AdminUserDO.class);
            rule.addDeptColumn(DeptDO.class, "id");
            // user
            rule.addUserColumn(AdminUserDO.class, "id");
        };
    }

}

2.2.9 YudaoDataPermissionAutoConfiguration

数据权限拦截器插件的配置类,将各种规则对象DataPermissionRule注入到DataPermissionRuleFactory规则工厂中,将数据权限拦截器DataPermissionInterceptor注册到mybatis-plus插件队列中。

@AutoConfiguration
public class YudaoDataPermissionAutoConfiguration {

    @Bean
    public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
        return new DataPermissionRuleFactoryImpl(rules);
    }

    @Bean
    public DataPermissionRuleHandler dataPermissionRuleHandler(MybatisPlusInterceptor interceptor,
                                                               DataPermissionRuleFactory ruleFactory) {
        // 创建 DataPermissionInterceptor 拦截器
        DataPermissionRuleHandler handler = new DataPermissionRuleHandler(ruleFactory);
        DataPermissionInterceptor inner = new DataPermissionInterceptor(handler);
        // 添加到 interceptor 中
        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
        MyBatisUtils.addInterceptor(interceptor, inner, 0);
        return handler;
    }

    .........

}

三、数据权限注解处理器

数据权限SQL拦截器将系统中定义了的全部数据权限规则适用于所有的场景,但是有些业务下的一些方法是不能适用某些数据权限的,例如某人在OA中只有个人数据权限,但是选择审批人时需要能找到他的领导,这时就需要对某些具体的业务方法进行特殊处理。

3.1 主要涉及类和接口

  • cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission 注解,加在类上或方法上,用于为某个具体的业务方法进行具体的数据权限规则控制。

  • cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor 类,封装一个切点(Pointcut)和通知(Advice)的Advisor接口,用于把DataPermission注解和注解处理器DataPermissionAnnotationInterceptor进行关联。

  • cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationInterceptor 类,DataPermission注解的处理器。

  • cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder 类,封装了透传数据权限注解的上下文对象ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS和一些操作它的方法。

3.2 实现原理

在线程上下文中维护一个LinkedList<DataPermission> list,带有DataPermission注解的方法执行前,注解处理器会拦截到并获取注解存入list,执行完成注解处理器还会再将对应注解从list中移除,由于方法执行顺序是栈结构,后进先出,因此维护注解的list也要和方法栈一样后进先出,这样数据权限SQL拦截器在DataPermissionRuleFactoryImpl中获取注解时便能获取到最近一级业务方法上的注解了。

实现比较抽象,伪代码举例说明:

@anno(scope = 1)
service1() {

    mapper.sql(a);

    @anno(scope = 2)
    service2() {
        mapper.sql(b);

        @anno(scope = 3)
        service3() {
            mapper.sql(c);
        }

        mapper.sql(d);
    }

    service4() {
        mapper.sql(e);
    }
}
  1. service1()执行mapper.sql(a);时,list中只有一个@anno(scope = 1)mapper.sql(a);适用@anno(scope = 1),执行到调用service2()时,service2()在方法栈的最外,list也会将@anno(scope = 2)维护在最外面,这样mapper.sql(b);被数据权限拦截器拦截时,从上下文获取到的就是@anno(scope = 2)service2()执行完成从方法栈退出,list也会将最外的@anno(scope = 2)移除,只剩下@anno(scope = 1),然后service1()继续执行调用service4(),因为service4()没有加注解,因此其中的mapper.sql(e);便依然适用@anno(scope = 1)

  2. service2()调用service3()也是一样的道理,由于service3()也加了注解,因此执行到调用service3()时,list最外面就是@anno(scope = 3)mapper.sql(c);适用@anno(scope = 3)service3()执行完毕退出方法栈后@anno(scope = 3)被从list移除,mapper.sql(d);便还适用@anno(scope = 2)

图例说明:

注解处理器根据被拦截的方法的入栈出栈顺序在线程上下文中同步维护了一个栈结构的list来存储从方法上获取的数据权限注解,方法进栈注解也”进栈”,方法出栈注解也”出栈”,注解处理器在业务方法前”抢先一步”获取业务方法上面的数据权限注解维护到线程上下文,mapper执行时数据权限SQL拦截器在规则工厂DataPermissionRuleFactoryImpl中从线程上下文获取到的,永远是”栈”的最顶部的那个注解,注解上设置的权限规则也刚好适用于当前mapper方法要执行的SQL,这一精巧的设计实现了对某个业务方法进行特殊的数据权限控制,而且可以保证加在最近一级方法上的注解优先生效。

3.3 源码解读

3.3.1 @DataPermission

自定义数据权限注解,可以配置数据权限是否开启、适用的数据权限规则有哪些和不适用的数据权限规则有哪些,DataPermissionRuleFactoryImpl从线程上下文获取到该注解后,根据这些属性和匹配逻辑进行进一步的数据权限规则处理。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {

    /**
     * 当前类或方法是否开启数据权限
     * 即使不添加 @DataPermission 注解,默认是开启状态
     * 可通过设置 enable 为 false 禁用
     */
    boolean enable() default true;

    /**
     * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()}
     */
    Class<? extends DataPermissionRule>[] includeRules() default {};

    /**
     * 排除的数据权限规则数组,优先级最低
     */
    Class<? extends DataPermissionRule>[] excludeRules() default {};

}

3.3.2 DataPermissionContextHolder

封装了一个LinkedList<DataPermission>到线程上下文ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS中,又封装了一些对LinkedList的进出栈操作。

public class DataPermissionContextHolder {

    /**
     * 使用 List 的原因,可能存在方法的嵌套调用
     */
    private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS =
            TransmittableThreadLocal.withInitial(LinkedList::new);

    /**
     * 获得当前的 DataPermission 注解
     *
     * @return DataPermission 注解
     */
    public static DataPermission get() {
        return DATA_PERMISSIONS.get().peekLast();
    }

    /**
     * 入栈 DataPermission 注解
     *
     * @param dataPermission DataPermission 注解
     */
    public static void add(DataPermission dataPermission) {
        DATA_PERMISSIONS.get().addLast(dataPermission);
    }

    /**
     * 出栈 DataPermission 注解
     *
     * @return DataPermission 注解
     */
    public static DataPermission remove() {
        DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast();
        // 无元素时,清空 ThreadLocal
        if (DATA_PERMISSIONS.get().isEmpty()) {
            DATA_PERMISSIONS.remove();
        }
        return dataPermission;
    }

    /**
     * 获得所有 DataPermission
     *
     * @return DataPermission 队列
     */
    public static List<DataPermission> getAll() {
        return DATA_PERMISSIONS.get();
    }

    /**
     * 清空上下文
     *
     * 目前仅仅用于单测
     */
    public static void clear() {
        DATA_PERMISSIONS.remove();
    }

}

解读:

  1. get()方法,取出最后一个添加进来的元素,用于DataPermissionRuleFactoryImpl从线程上下文获取到当前要执行的SQL适用的数据权限规则,当每个线程首次调用该方法时,ThreadLocal中的LinkedList<DataPermission>对象将被(new)创建。

  2. add(DataPermission dataPermission)方法,添加一个元素在最后面,用于注解处理器DataPermissionAnnotationInterceptor在方法开始前将方法上的注解存到LinkedList。

  3. remove()方法,删除最后一个添加进来的元素,用于注解处理器DataPermissionAnnotationInterceptor在方法执行结束后将当前方法上的注解从LinkedList中移除,当LinkedList里面没有元素时,说明方法栈最外层带有注解的业务方法也已经执行完毕,此时直接DATA_PERMISSIONS.remove()销毁线程上下文中的LinkedList。

3.3.3 DataPermissionAnnotationAdvisor

封装一个切点(Pointcut)和通知(Advice)的Advisor接口,用于指定DataPermission注解的处理器是DataPermissionAnnotationInterceptor

@Getter
@EqualsAndHashCode(callSuper = true)
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {

    private final Advice advice;

    private final Pointcut pointcut;

    public DataPermissionAnnotationAdvisor() {
        this.advice = new DataPermissionAnnotationInterceptor();
        this.pointcut = this.buildPointcut();
    }

    protected Pointcut buildPointcut() {
        Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
        Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
        return new ComposablePointcut(classPointcut).union(methodPointcut);
    }

}

解读:

  1. this.advice = new DataPermissionAnnotationInterceptor()指定了切面(通知)是DataPermissionAnnotationInterceptor

  2. this.pointcut = this.buildPointcut()指定了切点是new ComposablePointcut(classPointcut).union(methodPointcut)

  3. Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true) 注解加在类上,类(不含父类)中所有方法都将被注解处理器拦截

  4. Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); 只根据方法的注解来匹配且只匹配直接标注该注解的方法

  5. new ComposablePointcut(classPointcut).union(methodPointcut) 无论类上的注解还是只加在方法上的注解,都进行拦截

3.3.4 DataPermissionAnnotationInterceptor

注解处理器,加了注解的(类)方法被执行前后获取注解,保存到线程上下文的LinkedList<DataPermission>中。

@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {

    /**
     * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位
     */
    static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);

    @Getter
    private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        // 入栈
        DataPermission dataPermission = this.findAnnotation(methodInvocation);
        if (dataPermission != null) {
            DataPermissionContextHolder.add(dataPermission);
        }
        try {
            // 执行逻辑
            return methodInvocation.proceed();
        } finally {
            // 出栈
            if (dataPermission != null) {
                DataPermissionContextHolder.remove();
            }
        }
    }

    private DataPermission findAnnotation(MethodInvocation methodInvocation) {
        // 1. 从缓存中获取
        Method method = methodInvocation.getMethod();
        Object targetObject = methodInvocation.getThis();
        Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
        MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
        DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
        if (dataPermission != null) {
            return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
        }

        // 2.1 从方法中获取
        dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
        // 2.2 从类上获取
        if (dataPermission == null) {
            dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
        }
        // 2.3 添加到缓存中
        dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
        return dataPermission;
    }

}

解读:

  1. DataPermissionContextHolder.add(dataPermission) 执行前获取方法上的注解,存入线程上下文中的List(进栈)

  2. methodInvocation.proceed(); 目标方法执行

  3. DataPermissionContextHolder.remove(); 执行完毕后从线程上下文中的List移除(出栈)

3.3.5 YudaoDataPermissionAutoConfiguration

将数据权限注解处理器Advisor加入Spring容器

@AutoConfiguration
public class YudaoDataPermissionAutoConfiguration {

    .........

    @Bean
    public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
        return new DataPermissionAnnotationAdvisor();
    }

}

四、总结

数据权限是一个比较常用的功能,芋道源码通过mybatis-plus自定义插件在SQL执行前拦截并解析到对应的表,根据数据权限规则对这些表追加过滤条件来实现数据权限控制,对于一些需要单独指定数据权限的业务方法,通过数据权限注解和结合线程上下文对加了注解的方法进行前置和后置的处理,把当前方法适用的数据权限规则传递给数据权限SQL解析器进行额外处理,使得数据权限规则既能全局生效又能局部调整。


芋道源码解析之数据权限
https://blog.liuzijian.com/post/source-code-yudao-data-permission.html
作者
Liu Zijian
发布于
2025年1月22日
更新于
2025年2月26日
许可协议