数据权限

1,104 阅读4分钟

引言

基于MybatisPlusInterceptor技术实现的数据权限。通过在删除、更新、查询语句中增加where条件实现。数据权限是一个很大的概念,类似于这样的需求我们可以使用数据权限解决。

  1. 一般我们使用的都是基于部门、用户的数据权限。需要每个业务表单都要挂一个dept_id、user_id。
  2. 还有类似于数据字典管理,只能是创建者才能更新字典数据。
  3. crm系统只能看见自己创建的客户,上级可以看到下属的客户。
  4. OA系统自己发起的审批流程只能自己看到。HR可以看到所有人的审批流程。

数据权限实现原理.png

需求

在查询用户列表时,用户可以看到自己,可以看到同一部门下的用户,或者所有用户。都可以使用where dept_id = ?实现。主要表现为根据不同数据表,拼接不同的数据条件。不同的表可能部门id字段名字不同,就需要每个自定义模块配置DeptDataPermissionRuleCustomizer指定哪些表需要部门的数据权限。而且指定字段名是哪个。

DataPermissionRuleFactory规则配置

作用:存储不同dataPermissionRule数据规则的容器,提供管理能力。将所有自定义数据规则汇总。
实现:是在配置类中注入到IOC容器中。将数据规则List<DataPermissionRule> rules加入到DataPermissionRuleFactory类中。将DataPermissionRuleFactory作为参数传递给interceptor。

//注入自定义规则
@Bean
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
    return new DataPermissionRuleFactoryImpl(rules);
}
//注入拦截器
@Bean
public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor,
                                                                           List<DataPermissionRule> rules) {
    // 创建 DataPermissionDatabaseInterceptor 拦截器
    DataPermissionRuleFactory ruleFactory = dataPermissionRuleFactory(rules);
    DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory);
    // 添加到 interceptor 中
    // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
    MyBatisUtils.addInterceptor(interceptor, inner, 0);
    return inner;
}

List<DataPermissionRule> rules来自于注入到容器中实现DataPermissionRule接口的类。类似于

@Configuration
@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;
    }

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

}

结合@DataPermission注解。默认是所有方法都是开启数据权限的,根据在类上、方法上的注解排除数据权限规则。DataPermissionContextHolder,DataPermissionAnnotationAdvisor

核心流程

拦截器拦截sql

DataPermissionDatabaseInterceptor#beforeQuery和beforePrepare两个方法。在执行SQL语句前调用。

  1. 获取此方法上都有哪些数据规则。没有@DataPermission注解,表示开启规则。根据注解判断此方法要生效或者忽略哪些规则。
  2. mappedStatementCache:记录当前数据规则下,不需要重写SQL的id,这个id是在xml文件中编写的sql语句id。是一个缓存,下次通用执行这个方法时,就可以快速判断出是否需要重写where条件。
  3. ContextHolder:记录查询对应的规则和是否需要重新。只是为了透传数据。不然方法调用链中需要将数据规则一直传递
  4. JsqlParserSupport模板设计模式。根据语句的不同类型,对应不同的增、删、改、查方法。

重写where条件

  1. 基于上下文ContextHolder拿到权限规则列表,判断有没有规则作用于这张表上。
  2. protected Expression builderExpression(Expression currentExpression, List<Table> tables)原来的SQL语句可能是有where 语句的,那数据权限的SQL语句用and和他进行拼接。表名是list,因为一条SQL语句可能是多张表操作,且多张表都有对应的数据权限过滤条件。
  3. buildDataPermissionExpression 根据表名拼接过滤条件。条件来自于DatePermissionRule的getExpression

数据规则

DataPermissionRule接口,主要职责是

  1. 哪些数据库表,需要使用该数据权限规则。
  2. 当操作这些数据库表,需要额外拼接怎么样的 WHERE 条件

基于部门的数据权限

dept_id来自于哪里

先去登录用户的上下文LoginUser寻找,找不到的话,获取此用户的所属角色id列表。遍历每一个角色cn.iocoder.yudao.module.system.service.permission.PermissionServiceImpl#getDeptDataPermission的数据权限设置

技巧惰性求值

这个数据只有需要的时候才去数据库查询,有且仅有第一次发起DB查询

// 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询
Supplier<Long> userDeptIdCache = Suppliers.memoize(() -> userService.getUser(userId).getDeptId());