数据权限

数据行级权限是指不同的用户只能访问特定条件下的数据行。比如,部门经理可以查看其部门下的所有数据,而普通员工只能查看他自己提交的数据。

要在 pi-admin 中使用行级权限,你需要以下步骤:

  1. 数据库中定义与数据行级权限相关联的列

    数据权限相关的表中需要创建关联的列,比如部门、部门及下级部门、自定义部门等数据权限类型需要用到列 dept_id,本人数据权限类型需要用到 user_id

    `dept_id` bigint unsigned DEFAULT NULL COMMENT '部门 ID',
    `user_id` bigint unsigned DEFAULT NULL COMMENT '用户 ID',
    

    在插入时需要维护这些列的值。

  2. 在 Mapper 接口中指定支持数据权限的方法。

    @DataPermission({
        @DataPermissionItem(type = DataPermissionTypeEnum.SELF, 
                            @DataPermissionColumn(key = "userId", value = "user_id"))
        @DataPermissionItem(type = DataPermissionTypeEnum.DEPT_AND_CHILD, 
                            @DataPermissionColumn(key = "deptId", value = "dept_id"))
    })
    TestVO listTest(@Param("p1") ParamDTO p1);
    

    在以上代码中,listTest 支持两种数据权限类型:DataPermissionTypeEnum.SELFDataPermissionTypeEnum.DEPT_AND_CHILD,分别表示本人和部门及下级部门。

    扫描所有的 mapper 接口的时机是在程序启动时,这极大的提高了程序运行时执行的效率。有关如何在程序启动时扫描 mapper 的相关信息,请参阅数据权限实现原理

  3. 在角色管理中给角色配置角色范围,拥有对应角色的的用户就拥有对应的权限:

以上配置了拥有部门经理的用户执行 listTest 时能查看其部门及下级部门的数据。

对 MyBatis-Plus BaseMapper 中提供的原生方法进行过滤

对于 MyBatis-Plus 提供的原生的方法,需要先重写该方法,再对该方法进行注解:

DataPermissionTypeEnum

me.pi.admin.common.mybatis.enums.DataPermissionTypeEnum 中定义了数据权限类型:

/**
     * 全部
     */
ALL(1),
/**
     * 部门
     */
DEPT(2, "#{#deptId} = #{#data.deptId}", "dept_id = #{#data.deptId}"),
/**
     * 部门及下级部门
     */
DEPT_AND_CHILD(3, "#{#deptId} IN (#{@dps.getDeptAndChildId(#data.deptId)})",
               "dept_id IN (#{@dps.getDeptAndChildId(#data.deptId)})"),
/**
     * 自定义部门
     */
CUSTOM_DEPT(4, "#{#deptId} IN (#{@dps.getDeptIdsByRoleId(#data.roleId)})",
            "dept_id IN (#{@dps.getDeptIdsByRoleId(#data.roleId)})"),
/**
     * 本人
     */
SELF(5, "#{#userId} = #{#data.id}", "user_id = #{#data.userId}");

数据权限模板支持 spel 表达式,以 DEPT_AND_CHILD 为例,3 表示权限类型代码;#{#deptId} IN (#{@dps.getDeptAndChildId(#data.deptId)})表示数据权限 sql 模板;dept_id IN (#{@dps.getDeptAndChildId(#data.deptId)}) 表示默认 sql 模板(不需要指定 deptId 与数据库中列名的对应关系)。

数据范围可以通过 me.pi.admin.common.mybatis.annotation.DataPermissionItem 注解在 Mapper 接口的方法上指定,它接收两个参数:

public @interface DataPermissionItem {
    // 1. 数据权限类型
    DataPermissionTypeEnum type();
    // 2. 数据权限模板变量与列名的映射
    DataPermissionColumn column() default @DataPermissionColumn(key = "", value = "");
}

在上面的代码中, type 用于指定数据权限的类型,column 用于指定模板中的 key 对应的数据库表中的列名。

当数据权限配置如下,表示数据权限范围为部门及下级部门,sql 会被填充成 dept_id IN (#{@dps.getDeptAndChildId(#data.deptId)})

@DataPermission({
    @DataPermissionItem(type = DataPermissionTypeEnum.DEPT_AND_CHILD, 
                        @DataPermissionColumn(key = "deptId", value = "dept_id"))
})
TestVO listTest(@Param("p1") ParamDTO p1);

你可以更改生效的列,比如在数据库中表示部门 ID 的列名为 did,则可以通过指定 value 改变它,最终生成的 sql 为 did IN (#{@dps.getDeptAndChildId(#data.deptId)})

@DataPermission({
    @DataPermissionItem(type = DataPermissionTypeEnum.DEPT_AND_CHILD, 
                        @DataPermissionColumn(key = "deptId", value = "did"))
})
TestVO listTest(@Param("p1") ParamDTO p1);

如果没有指定 @DataPermissionColumn(key = "deptId", value = "dept_id"),则会使用默认 sql 模板,也就是 dept_id IN (#{@dps.getDeptAndChildId(#data.deptId)})

@DataPermission({
    @DataPermissionItem(type = DataPermissionTypeEnum.DEPT_AND_CHILD)
})
TestVO listTest(@Param("p1") ParamDTO p1);

#{@dps.getDeptAndChildId(#data.deptId)} 中的 @dps 表示 Spring 中的一个名为 dps 的 bean,它的类型为 me.pi.admin.core.system.service.DataPermissionService

public interface DataPermissionService {
    /**
     * 通过部门 ID 获取当前部门及下级部门的部门 ID 列表,以逗号分隔
     *
     * @param deptId 当前部门 ID
     * @return 当前部门及下级部门的部门 ID 列表,以逗号分隔
     */
    String getDeptAndChildId(Long deptId);

    /**
     * 通过角色 ID 获取部门 ID 列表
     * @param roleId 角色 ID
     * @return 部门 ID 列表
     */
    String getDeptIdsByRoleId(Long roleId);
}

通过 @Service("dps") 指定 bean 的名称。

扩展 DataPermissionData

对于模板中的 #data.deptId 的值则由 me.pi.admin.common.mybatis.handler.DataPermissionData 定义,如果需要扩展,只能修改源代码:

public class DataPermissionData {
    /**
     * 用户 ID
     */
    private Long userId;
    /**
     * 部门 ID
     */
    private Long deptId;
    /**
     * 角色 ID
     */
    private Long roleId;
}

赋值的逻辑在 me.pi.admin.common.mybatis.handler.PiDataPermissionHandler#getSqlConditions 中:

实现原理

实现数据权限的关键是拦截待执行的 SQL,动态添加查询条件,实现过滤数据的目的。要实现这一点,可以借助 MyBatis-Plus 的拦截器。

关键代码

类型 作用
me.pi.admin.common.mybatis.interceptor.PiDataPermissionInterceptor 数据权限拦截器
me.pi.admin.common.mybatis.handler.PiDataPermissionHandler 数据权限处理器
me.pi.admin.common.mybatis.enums.DataPermissionTypeEnum 数据权限类型枚举
me.pi.admin.common.mybatis.annotation.DataPermission 数据权限注解
me.pi.admin.common.mybatis.annotation.DataPermissionItem 配置应用的数据权限各类型
me.pi.admin.common.mybatis.annotation.DataPermissionColumn 指定数据权限模板中的 key 与数据库列的映射

扫描需要支持数据权限的 mappedStatement

程序启动时扫描所有的 mapper 接口,如果 mapper 接口的方法上标注了 me.pi.admin.common.mybatis.annotation.DataPermission 注解,则表明支持数据权限:

如何获取所有 mapper 接口?MyBatis-Plus 在启动时扫描了 mapper 接口,并缓存了起来,可以通过 sqlSessionFactory 获取:

Collection<Class<?>> mappers = sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers();

另外,通过反射可以获取方法上的注解信息,保存在 PiDataPermissionHandlerannotationCaches 中:

/**
 * 数据权限注解扫描器
 *
 * @author ZnPi
 * @date 2023-05-06
 */
@RequiredArgsConstructor
public class DataPermissionScanner implements CommandLineRunner {
    private final SqlSessionFactory sqlSessionFactory;
    private final PiDataPermissionHandler dataPermissionHandler;

    @Override
    public void run(String... args) {
        Collection<Class<?>> mappers = sqlSessionFactory.getConfiguration().getMapperRegistry().getMappers();
        mappers.forEach(mapper -> {
            Map<Method, Annotation> methodsAnnotation = AnnotationScanner.getMethodsAnnotation(mapper, DataPermission.class);
            methodsAnnotation.forEach((method, annotation) -> {
                String key = method.getDeclaringClass().getName() + "." + method.getName();
                // 将解析结果保存在 PiDataPermissionHandler 的 annotationCaches 中
                dataPermissionHandler.getAnnotationCaches().put(key, annotation);
            });
        });
    }
}
/**
  * 获取指定类上的方法上标注了指定注解的方法以及注解
  *
  * @param clazz      指定类的字节码
  * @param annotation 注解
  * @return 定类上的方法上标注了指定注解的方法以及注解
  */
public static Map<Method, Annotation> getMethodsAnnotation(
    Class<?> clazz, Class<? extends Annotation> annotation) {
    Map<Method, Annotation> methodAnnotationMap = new HashMap<>();
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for (Method method : declaredMethods) {
        if (method.isAnnotationPresent(annotation)) {
            methodAnnotationMap.put(method, method.getAnnotation(annotation));
        }
    }
    return methodAnnotationMap;
}

MyBatis-Plus 拦截器

数据权限拦截器可以参考 com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor 的实现:

  • 实现 com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor 接口。

    public interface InnerInterceptor {
       protected void processSelect(Select select, int index, String sql, Object obj) {
            throw new UnsupportedOperationException();
        }
        
        protected void processUpdate(Update update, int index, String sql, Object obj) {
            throw new UnsupportedOperationException();
        }
        
        protected void processUpdate(Update update, int index, String sql, Object obj) {
            throw new UnsupportedOperationException();
        }
    }
    
  • 继承 com.baomidou.mybatisplus.extension.parser.JsqlParserSupport

    /**
     * https://github.com/JSQLParser/JSqlParser
     */
    public abstract class JsqlParserSupport {
        
    }
    

拦截器实现请查看 me.pi.admin.common.mybatis.interceptor.PiDataPermissionInterceptor

获取数据权限范围

数据权限范围的配置方式请参考数据行级权限一节,源码请参考 me.pi.admin.common.mybatis.handler.PiDataPermissionHandler#getSqlConditions

// 获取注解中的数据权限项
List<DataPermissionItem> dataPermissionItems = Arrays.stream(annotation.value())
    .filter(dataPermissionItem ->
            dataPermissionType.getCode().equals(dataPermissionItem.type().getCode()))
    .collect(Collectors.toList());
if (dataPermissionItems.isEmpty()) {
    continue;
}

String sqlTemplate;

DataPermissionItem dataPermissionItem = dataPermissionItems.get(0);
if (!StringUtils.hasText(dataPermissionItem.column().key())) {
    // 未指定列,使用默认值
    sqlTemplate = dataPermissionType.getDefaultSqlTemplate();
} else {
    // 设置的 key 与模板不匹配
    if (!dataPermissionType.getSqlTemplate().contains(dataPermissionItem.column().key())) {
        continue;
    }

    sqlTemplate = dataPermissionType.getSqlTemplate();
    standardEvaluationContext.setVariable(dataPermissionItem.column().key(),
                                          dataPermissionItem.column().value());
}

项目实战

基于 Spring Boot 2.7.12、MyBatis-Plus、Spring Security 等主流技术栈构建的后台管理系统:

Gitee GitHub
后端 https://gitee.com/linjiabin100/pi-admin.git https://github.com/zengpi/pi-admin.git
前端 https://gitee.com/linjiabin100/pi-admin-web.git https://github.com/zengpi/pi-admin-web.git
posted @ 2023-06-16 13:41  ZnPi  阅读(302)  评论(0编辑  收藏  举报