初始化提交
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>vv-framework</artifactId>
|
||||
<groupId>com.vv.boot</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>vv-spring-boot-starter-biz-data-permission</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>数据权限</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.vv.boot</groupId>
|
||||
<artifactId>vv-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.vv.boot</groupId>
|
||||
<artifactId>vv-spring-boot-starter-security</artifactId>
|
||||
<optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 -->
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.vv.boot</groupId>
|
||||
<artifactId>vv-spring-boot-starter-mybatis</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.vv.start.framework.datapermission.config;
|
||||
|
||||
import cn.vv.start.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor;
|
||||
import cn.vv.start.framework.datapermission.core.db.DataPermissionRuleHandler;
|
||||
import cn.vv.start.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.vv.start.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import cn.vv.start.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl;
|
||||
import cn.vv.start.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据权限的自动配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AutoConfiguration
|
||||
public class VvDataPermissionAutoConfiguration {
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
|
||||
return new DataPermissionAnnotationAdvisor();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.vv.start.framework.datapermission.config;
|
||||
|
||||
import cn.vv.start.framework.common.biz.system.permission.PermissionCommonApi;
|
||||
import cn.vv.start.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
|
||||
import cn.vv.start.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
|
||||
import cn.vv.start.framework.security.core.LoginUser;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基于部门的数据权限 AutoConfiguration
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(LoginUser.class)
|
||||
@ConditionalOnBean(value = {DeptDataPermissionRuleCustomizer.class})
|
||||
public class VvDeptDataPermissionAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi,
|
||||
List<DeptDataPermissionRuleCustomizer> customizers) {
|
||||
// 创建 DeptDataPermissionRule 对象
|
||||
DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
|
||||
// 补全表配置
|
||||
customizers.forEach(customizer -> customizer.customize(rule));
|
||||
return rule;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cn.vv.start.framework.datapermission.core.annotation;
|
||||
|
||||
import cn.vv.start.framework.datapermission.core.rule.DataPermissionRule;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据权限注解
|
||||
* 可声明在类或者方法上,标识使用的数据权限规则
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@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 {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.vv.start.framework.datapermission.core.aop;
|
||||
|
||||
import cn.vv.start.framework.datapermission.core.annotation.DataPermission;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.support.AbstractPointcutAdvisor;
|
||||
import org.springframework.aop.support.ComposablePointcut;
|
||||
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
|
||||
|
||||
/**
|
||||
* {@link cn.vv.start.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cn.vv.start.framework.datapermission.core.aop;
|
||||
|
||||
import cn.vv.start.framework.datapermission.core.annotation.DataPermission;
|
||||
import lombok.Getter;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.core.MethodClassKey;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* {@link DataPermission} 注解的拦截器
|
||||
* 1. 在执行方法前,将 @DataPermission 注解入栈
|
||||
* 2. 在执行方法后,将 @DataPermission 注解出栈
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package cn.vv.start.framework.datapermission.core.aop;
|
||||
|
||||
import cn.vv.start.framework.datapermission.core.annotation.DataPermission;
|
||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link DataPermission} 注解的 Context 上下文
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package cn.vv.start.framework.datapermission.core.db;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.vv.start.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.vv.start.framework.datapermission.core.rule.DataPermissionRuleFactory;
|
||||
import cn.vv.start.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.schema.Table;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.vv.start.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck;
|
||||
|
||||
/**
|
||||
* 基于 {@link DataPermissionRule} 的数据权限处理器
|
||||
*
|
||||
* 它的底层,是基于 MyBatis Plus 的 <a href="https://baomidou.com/plugins/data-permission/">数据权限插件</a>
|
||||
* 核心原理:它会在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
|
||||
|
||||
private final DataPermissionRuleFactory ruleFactory;
|
||||
|
||||
@Override
|
||||
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
|
||||
// 特殊:跨租户访问
|
||||
if (skipPermissionCheck()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获得 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cn.vv.start.framework.datapermission.core.rule;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 数据权限规则接口
|
||||
* 通过实现接口,自定义数据规则。例如说,
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package cn.vv.start.framework.datapermission.core.rule;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link DataPermissionRule} 工厂接口
|
||||
* 作为 {@link DataPermissionRule} 的容器,提供管理能力
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface DataPermissionRuleFactory {
|
||||
|
||||
/**
|
||||
* 获得所有数据权限规则数组
|
||||
*
|
||||
* @return 数据权限规则数组
|
||||
*/
|
||||
List<DataPermissionRule> getDataPermissionRules();
|
||||
|
||||
/**
|
||||
* 获得指定 Mapper 的数据权限规则数组
|
||||
*
|
||||
* @param mappedStatementId 指定 Mapper 的编号
|
||||
* @return 数据权限规则数组
|
||||
*/
|
||||
List<DataPermissionRule> getDataPermissionRule(String mappedStatementId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.vv.start.framework.datapermission.core.rule;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.vv.start.framework.datapermission.core.annotation.DataPermission;
|
||||
import cn.vv.start.framework.datapermission.core.aop.DataPermissionContextHolder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 默认的 DataPermissionRuleFactoryImpl 实现类
|
||||
* 支持通过 {@link DataPermissionContextHolder} 过滤数据权限
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package cn.vv.start.framework.datapermission.core.rule.dept;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.vv.start.framework.common.biz.system.permission.PermissionCommonApi;
|
||||
import cn.vv.start.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
|
||||
import cn.vv.start.framework.common.enums.UserTypeEnum;
|
||||
import cn.vv.start.framework.common.util.collection.CollectionUtils;
|
||||
import cn.vv.start.framework.common.util.json.JsonUtils;
|
||||
import cn.vv.start.framework.datapermission.core.rule.DataPermissionRule;
|
||||
import cn.vv.start.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.vv.start.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.vv.start.framework.security.core.LoginUser;
|
||||
import cn.vv.start.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.NullValue;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
import net.sf.jsqlparser.expression.operators.relational.InExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 基于部门的 {@link DataPermissionRule} 数据权限规则实现
|
||||
*
|
||||
* 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
|
||||
*
|
||||
* 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
|
||||
* 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【vv-server 采用该方案】
|
||||
* 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】
|
||||
* 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】
|
||||
* 最终过滤条件是 WHERE dept_id = ?
|
||||
* 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号;
|
||||
* 最终过滤条件是 WHERE user_id IN (?, ?, ? ...)
|
||||
* 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤;
|
||||
* 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...)
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@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 PermissionCommonApi 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.vv.start.framework.datapermission.core.rule.dept;
|
||||
|
||||
/**
|
||||
* {@link DeptDataPermissionRule} 的自定义配置接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@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);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 基于部门的数据权限规则
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
package cn.vv.start.framework.datapermission.core.rule.dept;
|
||||
@@ -0,0 +1,73 @@
|
||||
package cn.vv.start.framework.datapermission.core.util;
|
||||
|
||||
import cn.vv.start.framework.datapermission.core.annotation.DataPermission;
|
||||
import cn.vv.start.framework.datapermission.core.aop.DataPermissionContextHolder;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* 数据权限 Util
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DataPermissionUtils {
|
||||
|
||||
private static DataPermission DATA_PERMISSION_DISABLE;
|
||||
|
||||
@DataPermission(enable = false)
|
||||
@SneakyThrows
|
||||
private static DataPermission getDisableDataPermissionDisable() {
|
||||
if (DATA_PERMISSION_DISABLE == null) {
|
||||
DATA_PERMISSION_DISABLE = DataPermissionUtils.class
|
||||
.getDeclaredMethod("getDisableDataPermissionDisable")
|
||||
.getAnnotation(DataPermission.class);
|
||||
}
|
||||
return DATA_PERMISSION_DISABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略数据权限,执行对应的逻辑
|
||||
*
|
||||
* @param runnable 逻辑
|
||||
*/
|
||||
public static void executeIgnore(Runnable runnable) {
|
||||
addDisableDataPermission();
|
||||
try {
|
||||
// 执行 runnable
|
||||
runnable.run();
|
||||
} finally {
|
||||
removeDataPermission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略数据权限,执行对应的逻辑
|
||||
*
|
||||
* @param callable 逻辑
|
||||
* @return 执行结果
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static <T> T executeIgnore(Callable<T> callable) {
|
||||
addDisableDataPermission();
|
||||
try {
|
||||
// 执行 callable
|
||||
return callable.call();
|
||||
} finally {
|
||||
removeDataPermission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加忽略数据权限
|
||||
*/
|
||||
public static void addDisableDataPermission(){
|
||||
DataPermission dataPermission = getDisableDataPermissionDisable();
|
||||
DataPermissionContextHolder.add(dataPermission);
|
||||
}
|
||||
|
||||
public static void removeDataPermission(){
|
||||
DataPermissionContextHolder.remove();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件
|
||||
*/
|
||||
package cn.vv.start.framework.datapermission;
|
||||
@@ -0,0 +1,2 @@
|
||||
cn.vv.start.framework.datapermission.config.VvDataPermissionAutoConfiguration
|
||||
cn.vv.start.framework.datapermission.config.VvDeptDataPermissionAutoConfiguration
|
||||
Reference in New Issue
Block a user