本文源码地址
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 |
数据行级权限是指不同的用户只能访问特定条件下的数据行。比如,部门经理可以查看其部门下的所有数据,而普通员工只能查看他自己提交的数据。
要在 pi-admin 中使用行级权限,你需要以下步骤:
-
数据库中定义与数据行级权限相关联的列
数据权限相关的表中需要创建关联的列,比如部门、部门及下级部门、自定义部门等数据权限类型需要用到列
dept_id
,本人数据权限类型需要用到user_id
:`dept_id` bigint unsigned DEFAULT NULL COMMENT '部门 ID', `user_id` bigint unsigned DEFAULT NULL COMMENT '用户 ID',
在插入时需要维护这些列的值。
-
在 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.SELF
、DataPermissionTypeEnum.DEPT_AND_CHILD
,分别表示本人和部门及下级部门。扫描所有的 mapper 接口的时机是在程序启动时,这极大的提高了程序运行时执行的效率。有关如何在程序启动时扫描 mapper 的相关信息,请参阅数据权限实现原理
-
在角色管理中给角色配置角色范围,拥有对应角色的的用户就拥有对应的权限:
以上配置了拥有部门经理的用户执行 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();
另外,通过反射可以获取方法上的注解信息,保存在 PiDataPermissionHandler
的 annotationCaches
中:
/**
* 数据权限注解扫描器
*
* @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());
}
标签:deptId,pi,admin,权限,数据,id
From: https://www.cnblogs.com/zn-pi/p/17485342.html