若依(RUOYI)

Springboot管理系统数据权限过滤(三)——0业务入侵实现部门数据权限过滤

上一章,讲解了SQL拦截器的,做了一个简单的SQL改造。本章要实现:仅对指定的service方法内执行的SQL进行处理;完成对部门权限的过滤;以下简单用一个图说明了整个处理过程,红色框住的部分,就是本章要实现的内容:Spring注解拦截器,该拦截器的目标是对添加了@DataScope注解的方法,作用是解析中DataScope对象,将放到线程变量的权限过滤对象栈中。以便在SQL拦截器中获取权限对象进

上一章,讲解了SQL拦截器的,做了一个简单的SQL改造。本章要实现:


仅对指定的service方法内执行的SQL进行处理;

完成对部门权限的过滤;

以下简单用一个图说明了整个处理过程,红色框住的部分,就是本章要实现的内容:


Spring注解拦截器,该拦截器的目标是对添加了@DataScope注解的方法,作用是解析中DataScope对象,将放到线程变量的权限过滤对象栈中。以便在SQL拦截器中获取权限对象进行解析;

DataScope栈,需要实现一个线程栈,没有业务逻辑,仅为传递作用;

SQL拦截器内,解析DataScope对象获得有权限的部门ID,然后拼接过滤条件;

另外DataScope权限注解也需要添加;

bded91134b8241f2351ec15bdb309a4f.png

  1. 直接上干货:

定义DataSopce对象

DataScope.java
DataScope对象里面设置了用于数据权限规则数组。在SQL拦截器中将从这些数据权限规则中获取条件表达式。

package com.luo.chengrui.labs.lab02.annotation;

import com.luo.chengrui.labs.lab02.datapermission.DataPermissionRule;

import java.lang.annotation.*;

/**
 * 数据权限过滤注解
 *
 * @author ruoyi
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DataScope
{


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

    /**
     * 生效的数据权限规则数组,为了以后方便扩展,所以定义为权限解析对象数组
     */
    Class<? extends DataPermissionRule>[] includeRules() default {};
}

DataPermissionRule.java
这里定义了一个权限解析接口,如此即可以扩展很多不同类型的权限,如:对部门过滤,本章我们仅实现对部门权限过滤。(若要实现对如预算科目、项目等过滤,可实现该接口,另外还得单独实现对这些数据权限的分配功能。若依仅实现了对部门和人员控制)

package com.luo.chengrui.labs.lab02.datapermission;

import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;

import java.util.Set;

/**
 * 数据权限规则接口
 * 通过实现接口,自定义数据规则。例如说,
 *
 * @author yudao
 */
public interface DataPermissionRule {


    /**
     * 根据表名和别名,生成对应的 WHERE / OR 过滤条件
     *
     * @param tableName 表名
     * @param tableAlias 别名,可能为空
     * @return 过滤条件 Expression 表达式
     */
    Expression getExpression(String tableName, Alias tableAlias);

}


DeptDataPermissionRule.java

部门权限解析实现类,只要SQL语句中有sys_dept表或者dept_id字段,则均可以通过getExpression(String tableName, Alias tableAlias)获取部门数据权限,并生成Express表达式。


ruoyi框架中数据权限分配和权限获取实现,可参考:Springboot管理系统数据权限过滤——ruoyi实现方案

package com.luo.chengrui.labs.lab02.datapermission.dept;

import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.luo.chengrui.labs.lab02.datapermission.DataPermissionRule;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 基于部门的 {@link DataPermissionRule} 数据权限规则实现
 * <p>
 * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
 * <p>
 */
@AllArgsConstructor
@Slf4j
public class DeptDataPermissionRule implements DataPermissionRule {


    static final Expression EXPRESSION_NULL = new NullValue();


    /**
     * 基于部门的表字段配置
     * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。
     * <p>
     * key:表名
     * value:字段名
     */
    private final Map<String, String> deptColumns = new HashMap<>();
    /**
     * 所有表名,需要进行权限过滤的表名
     */
    private final Set<String> TABLE_NAMES = new HashSet<>();
    /**
     * 添加需要过滤部门权限的表名和部门ID字段名
     * @param tableName
     * @param columnName
     */
    public void addDeptColumn(String tableName, String columnName) {
        deptColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }


    /**
     * 获取所有需要按部门权限过滤的表
     * @return
     */
    @Override
    public Set<String> getTableNames() {
        return TABLE_NAMES;
    }

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {

        // 情况三,拼接 Dept 和 User 的条件,最后组合
        Set<Long> deptIds = new HashSet<>();
        //模拟数据,实现业务中需要获取当前用户部门权限,获取到部门id。
        deptIds.add(1L);
        deptIds.add(2L);
        // 配置中包含表时,进行过滤
        if (Objects.nonNull(deptColumns.get(tableName))) {
            return new InExpression(buildColumn(tableName, tableAlias, deptColumns.get(tableName)),
                    new ExpressionList(deptIds.stream().map(LongValue::new).collect(Collectors.toList())));
        }
        return EXPRESSION_NULL;
        //


    }

    /**
     * 构建 Column 对象
     *
     * @param tableName  表名
     * @param tableAlias 别名
     * @param column     字段名
     * @return Column 对象
     */
    public static Column buildColumn(String tableName, Alias tableAlias, String column) {
        if (tableAlias != null) {
            tableName = tableAlias.getName();
        }
        return new Column(tableName + StringPool.DOT + column);
    }
}

数据权限线程变量

DataPermissionContextHolder.java

package com.luo.chengrui.labs.lab02.interceptor;

import com.luo.chengrui.labs.lab02.annotation.DataScope;

import java.util.LinkedList;
import java.util.List;

/**
 * {@link DataScope} 注解的 Context 上下文
 * 将方法上的注解对象设置到 线程变量里面,在SQL执行拦截器中获取注解对象,根据内容生成相应的权限。
 * 告诉SQL执行{DataPermissionDatabaseInterceptor}拦截器,如此方法需要添加权限。
 */
public class DataPermissionContextHolder {

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

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

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

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

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

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

}


定义DataScope注解拦截器

DataScopeAnnotationInterceptor.java
方法拦截器:作用是拦截添加了@DataScope注解的方法,获取DataScope对象,放入线程变量。

package com.luo.chengrui.labs.lab02.interceptor;

import com.luo.chengrui.labs.lab02.annotation.DataScope;
import lombok.Getter;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 DataScope} 注解的拦截器
 * 1. 在执行方法前,将 @DataPermission 注解入栈
 * 2. 在执行方法后,将 @DataPermission 注解出栈
 *
 * @author yudao
 */
public class DataScopeAnnotationInterceptor implements MethodInterceptor {

    /**
     * DataPermission 空对象,用于方法无 {@link import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;} 注解时,使用 DATA_PERMISSION_NULL 进行占位
     */
    static final DataScope DATA_PERMISSION_NULL = DataScopeAnnotationInterceptor.class.getAnnotation(DataScope.class);

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

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Logger log = LoggerFactory.getLogger(DataScopeAnnotationInterceptor.class);
        log.debug("DataScopeAnnotationInterceptor 拦截器:" + methodInvocation.getMethod().getName());
        // 入栈
        DataScope dataPermission = this.findAnnotation(methodInvocation);
        if (dataPermission != null) {
            DataPermissionContextHolder.add(dataPermission);
        }
        try {
            // 执行逻辑
            return methodInvocation.proceed();
        } finally {
            // 出栈
            if (dataPermission != null) {
                DataPermissionContextHolder.remove();
            }
        }
    }

    private DataScope 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);
        DataScope dataPermission = dataPermissionCache.get(methodClassKey);
        if (dataPermission != null) {
            return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
        }

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

}


DataPermissionAnnotationAdvisor.java
定义拦截点,对添加了DataScope注解的类和方法都添加了拦截点。

package com.luo.chengrui.labs.lab02.interceptor;

import com.luo.chengrui.labs.lab02.annotation.DataScope;
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;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component;

/**
 * {@link DataScope} 注解的 Advisor 实现类
 *
 */
@Getter
@EqualsAndHashCode(callSuper = true)
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {

    private final Advice advice;

    private final Pointcut pointcut;

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

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

}


SQL拦截器改造sql。

DataPermissionDatabaseInterceptor.java

代码比较多,主要是对sql各种情况的解析和处理,与实现业务关联性很弱。在实际业务扩展时,该类基本不需要进行修改。可能搜索对 getExpression 方法的调用,改造sql的位置。

package com.luo.chengrui.labs.lab02.datapermission;

import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.i

原创不易,完成人机校验,阅读全文

相关推荐