Springboot管理系统数据权限过滤(四)——mybatis数据权限插件

news2024/11/18 17:49:13

上一章,Springboot管理系统数据权限过滤(三)——0业务入侵实现部门数据权限过滤数据权限实现的思路和代码实现已经了解。本节在此基础上实现支持mybatis框架的通用数据过滤插件。
实现目标:

  • 业务系统引入cr-datapermission.jar插件包;
  • 通过默认数据权限规则可实现多种数据权限过滤;(ruoyi的部门权限、项目、租户过滤等)

上一章示例,使用的是mytatisplus数据持久框架,使用是的mybatisplus拦截器,本章是基于mybatis持久化框,所以仅拦截器实现上有一点点差异,但我们也是可以引入mybatisplus的依赖包,但仅使用它的对SQL的处理上,这样可以省很多事。除了这一点,其他的代码都可以沿用上一章的内容。

一、项目结构:

该项目初衷是把数据权限模板做成 自动配置 插件,对springboot版本有要求。但在springboot版本不支持的情况下,也可以通过配置创建相应bean也可以实现权限管理。
在这里插入图片描述

二、代码实现

2.1 pom.xml

<?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>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <!--        <version>2.0.0.RELEASE</version>-->
        <version>2.7.14</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.chengrui.tool</groupId>
    <artifactId>cr-datapermission</artifactId>
    <version>1.0.7</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring.boot.version>2.7.14</spring.boot.version>
        <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>

        <spring.boot.version>2.7.14</spring.boot.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>${spring.boot.version}</version>
            <!--            <version>2.0.0.RELEASE</version>-->
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.github.jsqlparser</groupId>
            <artifactId>jsqlparser</artifactId>
            <version>4.3</version>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>

        <!-- 实现对数据库连接池的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency> <!-- 本示例,我们使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
            <optional>true</optional>
        </dependency>

        <!-- 实现对 MyBatis 的自动化配置 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.2</version>
        </dependency>
        <!-- 方便等会写单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

2.2 DataPermission.java

没有任何内容的注解定义,可以定义相关字段,以实现更精细的数据过滤,支持更多的可配置性。

package com.luo.chengrui.datapermission.annotation;

import com.luo.chengrui.datapermission.rules.DataPermissionRule;

import java.lang.annotation.*;

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

}

2.3DataPermissionInterceptorConfiguration.java

这是自动配置,仅配置了注解拦截器。只要引用了该jar包则会自动配置DataPermissionAnnotationAdvisor Bean。即可拦截还有@DataPermission注解的方法了。

package com.luo.chengrui.datapermission.config;
import com.luo.chengrui.datapermission.interceptor.DataPermissionAnnotationAdvisor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
 * 配置mybatisPlusInterceptor
 */
@AutoConfiguration
public class DataPermissionInterceptorConfiguration {
    /**
     * 权限注解拦截器。
     *
     * @return
     */
    @Bean
    @Role(2)
    public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() {
        return new DataPermissionAnnotationAdvisor();
    }
}

2.4 DataPermissionAnnotationAdvisor.java

spring Advisor用来配置拦截点和拦截器的实现类。

package com.luo.chengrui.datapermission.interceptor;

import com.luo.chengrui.datapermission.annotation.DataPermission;
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 DataPermission} 注解的 Advisor 实现类
 */
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
    private final Advice advice;
    private final Pointcut pointcut;
    @Override
    public Advice getAdvice() {
        return advice;
    }
    @Override
    public Pointcut getPointcut() {
        return pointcut;
    }
    public DataPermissionAnnotationAdvisor() {
        this.advice = new DataScopeAnnotationInterceptor();
        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);
    }

}

2.5 DataPermissionAnnotationInterceptor.java

当方法体上有DataPermission注解时,方法执行前先执行该拦截器,解析出DataPermission的对象,并存放到线程变量中DataPermissionContextHolder。当方法执行完时,清除线程变量数据。

package com.luo.chengrui.datapermission.interceptor;

import com.luo.chengrui.datapermission.annotation.DataPermission;
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 DataPermission} 注解的拦截器
 * 1. 在执行方法前,将 @DataPermission 注解入栈
 * 2. 在执行方法后,将 @DataPermission 注解出栈
 *
 * @author luodz
 */
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {

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

    private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();

    public Map<MethodClassKey, DataPermission> getDataPermissionCache() {
        return dataPermissionCache;
    }

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Logger log = LoggerFactory.getLogger(DataPermissionAnnotationInterceptor.class);
        log.debug("DataPermissionAnnotationInterceptor 拦截器:" + methodInvocation.getMethod().getName());
        // 入栈
        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;
    }
}

2.6 DataPermissionContextHolder.java

用来缓存DataPermission权限注解,注解拦截器将DataPermission对象存入线程变量中,当在执行SQL拦截器时,从线程变量中获取DataPermission对象,如果能获取到,则进行数据过滤,如果没有获取到,则不做数据过滤。

package com.luo.chengrui.datapermission.interceptor;

import com.luo.chengrui.datapermission.annotation.DataPermission;

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

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

    /**
     * 使用 List 的原因,可能存在方法的嵌套调用
     */
    private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS =
            ThreadLocal.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();
    }

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

}

2.7 DataPermissionSqlParserInterceptor.java

这个是Mybatis拦截器的实现,

  • JsqlParserSupport:是mybatisplus的sql解析类,方便sql解析,别无他用;
  • Interceptor :是mybatis拦截器接口。

拦截器入口,实现了Interceptor接口中的方法:

 @Override
    public Object intercept(Invocation invocation) throws Throwable {
    }

DataPermissionSqlParserInterceptor.java 的完整代码

package com.luo.chengrui.datapermission.interceptor;

import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.luo.chengrui.datapermission.annotation.DataPermission;
import com.luo.chengrui.datapermission.rules.DataPermissionRule;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.NotExpression;
import net.sf.jsqlparser.expression.Parenthesis;
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.cache.CacheKey;
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.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.sql.Connection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现
 * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, List)} 方法
 * 主要是在执行SQL前拦截器,在执行之前可重写SQL
 *
 * @author yudao,luodz
 */

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class DataPermissionSqlParserInterceptor extends JsqlParserSupport implements Interceptor {

    private Logger logger = LoggerFactory.getLogger(DataPermissionSqlParserInterceptor.class);

    private static final String MYSQL_ESCAPE_CHARACTER = "`";
    private final List<DataPermissionRule> dataPermissionRule;

    public DataPermissionSqlParserInterceptor(List<DataPermissionRule> dataPermissionRule) {
        this.dataPermissionRule = dataPermissionRule;
    }

    private final MappedStatementCache mappedStatementCache = new MappedStatementCache();


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //线程变量中获取权限注解。
        // 根据业务场景要求,决定是否依赖于方法上权限注解。
        DataPermission saDataPermission = DataPermissionContextHolder.get();
        if (saDataPermission == null) {
            // 方法上未设置权限注解时,不处理SQL
            return invocation.proceed();
        }
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        if (target instanceof Executor) {
            Executor executor = (Executor) target;
            Object parameter = args[1];
            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                } else {
                    boundSql = (BoundSql) args[5];
                }

                this.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);

                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }

        } else {
            StatementHandler sh = (StatementHandler) target;
            if (null == args) {

            } else {
                Connection connections = (Connection) args[0];
                Integer transactionTimeout = (Integer) args[1];
                this.beforePrepare(sh, connections, transactionTimeout);
            }
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return !(target instanceof Executor) && !(target instanceof StatementHandler) ? target : Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 获取配置文件中的属性值
    }


    // SELECT 场景
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        // 获得 Mapper 对应的数据权限的规则
        if (mappedStatementCache.noRewritable(ms, dataPermissionRule)) { // 如果无需重写,则跳过
            return;
        }

        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        try {
            // 初始化上下文
            ContextHolder.init(dataPermissionRule);
            // 处理 SQL
            mpBs.sql(parserSingle(mpBs.sql(), null));
        } finally {
            // 添加是否需要重写的缓存
            addMappedStatementCache(ms);
            // 清空上下文
            ContextHolder.clear();
        }
    }

    // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限)
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            // 获得 Mapper 对应的数据权限的规则
            if (mappedStatementCache.noRewritable(ms, dataPermissionRule)) { // 如果无需重写,则跳过
                return;
            }

            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            try {
                // 初始化上下文
                ContextHolder.init(dataPermissionRule);
                // 处理 SQL
                mpBs.sql(parserMulti(mpBs.sql(), null));
            } finally {
                // 添加是否需要重写的缓存
                addMappedStatementCache(ms);
                // 清空上下文
                ContextHolder.clear();
            }
        }
    }

    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        processSelectBody(select.getSelectBody());
        List<WithItem> withItemsList = select.getWithItemsList();
        if (!com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(withItemsList)) {
            withItemsList.forEach(this::processSelectBody);
        }
    }

    /**
     * update 语句处理
     */
    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        final Table table = update.getTable();
        update.setWhere(this.builderExpression(update.getWhere(), table));
    }

    /**
     * delete 语句处理
     */
    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable()));
    }

    // ========== 和 TenantLineInnerInterceptor 一致的逻辑 ==========

    protected void processSelectBody(SelectBody selectBody) {
        if (selectBody == null) {
            return;
        }
        if (selectBody instanceof PlainSelect) {
            processPlainSelect((PlainSelect) selectBody);
        } else if (selectBody instanceof WithItem) {
            WithItem withItem = (WithItem) selectBody;
            processSelectBody(withItem.getSubSelect().getSelectBody());
        } else {
            SetOperationList operationList = (SetOperationList) selectBody;
            List<SelectBody> selectBodyList = operationList.getSelects();
            if (!CollectionUtils.isEmpty(selectBodyList)) {
                selectBodyList.forEach(this::processSelectBody);
            }
        }
    }

    /**
     * 处理 PlainSelect
     */
    protected void processPlainSelect(PlainSelect plainSelect) {
        //#3087 github
        List<SelectItem> selectItems = plainSelect.getSelectItems();
        if (!CollectionUtils.isEmpty(selectItems)) {
            selectItems.forEach(this::processSelectItem);
        }

        // 处理 where 中的子查询
        Expression where = plainSelect.getWhere();
        processWhereSubSelect(where);

        // 处理 fromItem
        FromItem fromItem = plainSelect.getFromItem();
        List<Table> list = processFromItem(fromItem);
        List<Table> mainTables = new ArrayList<>(list);

        // 处理 join
        List<Join> joins = plainSelect.getJoins();
        if (!CollectionUtils.isEmpty(joins)) {
            mainTables = processJoins(mainTables, joins);
        }

        // 当有 mainTable 时,进行 where 条件追加
        if (!CollectionUtils.isEmpty(mainTables)) {
            plainSelect.setWhere(builderExpression(where, mainTables));
        }
    }

    private List<Table> processFromItem(FromItem fromItem) {
        // 处理括号括起来的表达式
        while (fromItem instanceof ParenthesisFromItem) {
            fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
        }

        List<Table> mainTables = new ArrayList<>();
        // 无 join 时的处理逻辑
        if (fromItem instanceof Table) {
            Table fromTable = (Table) fromItem;
            mainTables.add(fromTable);
        } else if (fromItem instanceof SubJoin) {
            // SubJoin 类型则还需要添加上 where 条件
            List<Table> tables = processSubJoin((SubJoin) fromItem);
            mainTables.addAll(tables);
        } else {
            // 处理下 fromItem
            processOtherFromItem(fromItem);
        }
        return mainTables;
    }

    /**
     * 处理where条件内的子查询
     * <p>
     * 支持如下:
     * 1. in
     * 2. =
     * 3. >
     * 4. <
     * 5. >=
     * 6. <=
     * 7. <>
     * 8. EXISTS
     * 9. NOT EXISTS
     * <p>
     * 前提条件:
     * 1. 子查询必须放在小括号中
     * 2. 子查询一般放在比较操作符的右边
     *
     * @param where where 条件
     */
    protected void processWhereSubSelect(Expression where) {
        if (where == null) {
            return;
        }
        if (where instanceof FromItem) {
            processOtherFromItem((FromItem) where);
            return;
        }
        if (where.toString().indexOf("SELECT") > 0) {
            // 有子查询
            if (where instanceof net.sf.jsqlparser.expression.BinaryExpression) {
                // 比较符号 , and , or , 等等
                net.sf.jsqlparser.expression.BinaryExpression expression = (net.sf.jsqlparser.expression.BinaryExpression) where;
                processWhereSubSelect(expression.getLeftExpression());
                processWhereSubSelect(expression.getRightExpression());
            } else if (where instanceof InExpression) {
                // in
                InExpression expression = (InExpression) where;
                Expression inExpression = expression.getRightExpression();
                if (inExpression instanceof SubSelect) {
                    processSelectBody(((SubSelect) inExpression).getSelectBody());
                }
            } else if (where instanceof ExistsExpression) {
                // exists
                ExistsExpression expression = (ExistsExpression) where;
                processWhereSubSelect(expression.getRightExpression());
            } else if (where instanceof NotExpression) {
                // not exists
                NotExpression expression = (NotExpression) where;
                processWhereSubSelect(expression.getExpression());
            } else if (where instanceof Parenthesis) {
                Parenthesis expression = (Parenthesis) where;
                processWhereSubSelect(expression.getExpression());
            }
        }
    }

    protected void processSelectItem(SelectItem selectItem) {
        if (selectItem instanceof SelectExpressionItem) {
            SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
            if (selectExpressionItem.getExpression() instanceof SubSelect) {
                processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody());
            } else if (selectExpressionItem.getExpression() instanceof Function) {
                processFunction((Function) selectExpressionItem.getExpression());
            }
        }
    }

    /**
     * 处理函数
     * <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p>
     * <p> fixed gitee pulls/141</p>
     *
     * @param function
     */
    protected void processFunction(Function function) {
        ExpressionList parameters = function.getParameters();
        if (parameters != null) {
            parameters.getExpressions().forEach(expression -> {
                if (expression instanceof SubSelect) {
                    processSelectBody(((SubSelect) expression).getSelectBody());
                } else if (expression instanceof Function) {
                    processFunction((Function) expression);
                }
            });
        }
    }

    /**
     * 处理子查询等
     */
    protected void processOtherFromItem(FromItem fromItem) {
        // 去除括号
        while (fromItem instanceof ParenthesisFromItem) {
            fromItem = ((ParenthesisFromItem) fromItem).getFromItem();
        }

        if (fromItem instanceof SubSelect) {
            SubSelect subSelect = (SubSelect) fromItem;
            if (subSelect.getSelectBody() != null) {
                processSelectBody(subSelect.getSelectBody());
            }
        } else if (fromItem instanceof ValuesList) {
            logger.debug("Perform a subQuery, if you do not give us feedback");
        } else if (fromItem instanceof LateralSubSelect) {
            LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem;
            if (lateralSubSelect.getSubSelect() != null) {
                SubSelect subSelect = lateralSubSelect.getSubSelect();
                if (subSelect.getSelectBody() != null) {
                    processSelectBody(subSelect.getSelectBody());
                }
            }
        }
    }

    /**
     * 处理 sub join
     *
     * @param subJoin subJoin
     * @return Table subJoin 中的主表
     */
    private List<Table> processSubJoin(SubJoin subJoin) {
        List<Table> mainTables = new ArrayList<>();
        if (subJoin.getJoinList() != null) {
            List<Table> list = processFromItem(subJoin.getLeft());
            mainTables.addAll(list);
            mainTables = processJoins(mainTables, subJoin.getJoinList());
        }
        return mainTables;
    }

    /**
     * 处理 joins
     *
     * @param mainTables 可以为 null
     * @param joins      join 集合
     * @return List<Table> 右连接查询的 Table 列表
     */
    private List<Table> processJoins(List<Table> mainTables, List<Join> joins) {
        // join 表达式中最终的主表
        Table mainTable = null;
        // 当前 join 的左表
        Table leftTable = null;

        if (mainTables == null) {
            mainTables = new ArrayList<>();
        } else if (mainTables.size() == 1) {
            mainTable = mainTables.get(0);
            leftTable = mainTable;
        }

        //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
        Deque<List<Table>> onTableDeque = new LinkedList<>();
        for (Join join : joins) {
            // 处理 on 表达式
            FromItem joinItem = join.getRightItem();

            // 获取当前 join 的表,subJoint 可以看作是一张表
            List<Table> joinTables = null;
            if (joinItem instanceof Table) {
                joinTables = new ArrayList<>();
                joinTables.add((Table) joinItem);
            } else if (joinItem instanceof SubJoin) {
                joinTables = processSubJoin((SubJoin) joinItem);
            }

            if (joinTables != null) {

                // 如果是隐式内连接
                if (join.isSimple()) {
                    mainTables.addAll(joinTables);
                    continue;
                }

                // 当前表是否忽略
                Table joinTable = joinTables.get(0);

                List<Table> onTables = null;
                // 如果不要忽略,且是右连接,则记录下当前表
                if (join.isRight()) {
                    mainTable = joinTable;
                    if (leftTable != null) {
                        onTables = Collections.singletonList(leftTable);
                    }
                } else if (join.isLeft()) {
                    onTables = Collections.singletonList(joinTable);
                } else if (join.isInner()) {
                    if (mainTable == null) {
                        onTables = Collections.singletonList(joinTable);
                    } else {
                        onTables = Arrays.asList(mainTable, joinTable);
                    }
                    mainTable = null;
                }

                mainTables = new ArrayList<>();
                if (mainTable != null) {
                    mainTables.add(mainTable);
                }

                // 获取 join 尾缀的 on 表达式列表
                Collection<Expression> originOnExpressions = join.getOnExpressions();
                // 正常 join on 表达式只有一个,立刻处理
                if (originOnExpressions.size() == 1 && onTables != null) {
                    List<Expression> onExpressions = new LinkedList<>();
                    onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables));
                    join.setOnExpressions(onExpressions);
                    leftTable = joinTable;
                    continue;
                }
                // 表名压栈,忽略的表压入 null,以便后续不处理
                onTableDeque.push(onTables);
                // 尾缀多个 on 表达式的时候统一处理
                if (originOnExpressions.size() > 1) {
                    Collection<Expression> onExpressions = new LinkedList<>();
                    for (Expression originOnExpression : originOnExpressions) {
                        List<Table> currentTableList = onTableDeque.poll();
                        if (CollectionUtils.isEmpty(currentTableList)) {
                            onExpressions.add(originOnExpression);
                        } else {
                            onExpressions.add(builderExpression(originOnExpression, currentTableList));
                        }
                    }
                    join.setOnExpressions(onExpressions);
                }
                leftTable = joinTable;
            } else {
                processOtherFromItem(joinItem);
                leftTable = null;
            }
        }

        return mainTables;
    }

    // ========== 和 TenantLineInnerInterceptor 存在差异的逻辑:关键,实现权限条件的拼接 ==========

    /**
     * 处理条件
     *
     * @param currentExpression 当前 where 条件
     * @param table             单个表
     */
    protected Expression builderExpression(Expression currentExpression, Table table) {
        return this.builderExpression(currentExpression, Collections.singletonList(table));
    }

    /**
     * 处理条件
     *
     * @param currentExpression 当前 where 条件
     * @param tables            多个表
     */
    protected Expression builderExpression(Expression currentExpression, List<Table> tables) {
        // 没有表需要处理直接返回
        if (CollectionUtils.isEmpty(tables)) {
            return currentExpression;
        }

        // 第一步,获得 Table 对应的数据权限条件
        Expression dataPermissionExpression = null;
        for (Table table : tables) {
            // 构建每个表的权限 Expression 条件
            Expression expression = buildDataPermissionExpression(table);
            if (expression == null) {
                continue;
            }
            // 合并到 dataPermissionExpression 中
            dataPermissionExpression = dataPermissionExpression == null ? expression
                    : new AndExpression(dataPermissionExpression, expression);
        }

        // 第二步,合并多个 Expression 条件
        if (dataPermissionExpression == null) {
            return currentExpression;
        }
        if (currentExpression == null) {
            return dataPermissionExpression;
        }
        // ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression
        if (currentExpression instanceof OrExpression) {
            return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression);
        }
        // ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression
        return new AndExpression(currentExpression, dataPermissionExpression);
    }

    /**
     * 构建指定表的数据权限的 Expression 过滤条件
     *
     * @param table 表
     * @return Expression 过滤条件
     */
    private Expression buildDataPermissionExpression(Table table) {
        // 生成条件
        Expression allExpression = null;
        for (DataPermissionRule rule : ContextHolder.getRules()) {
            // 判断表名是否匹配
            if (!rule.getTableNames().contains(table.getName())) {
                continue;
            }
            // 如果有匹配的规则,说明可重写。
            // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。
            // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。
            ContextHolder.setRewrite(true);

            // 单条规则的条件
            String tableName = getTableName(table);
            Expression oneExpress = rule.getExpression(tableName, table.getAlias());
            if (oneExpress == null) {
                continue;
            }
            // 拼接到 allExpression 中
            allExpression = allExpression == null ? oneExpress
                    : new AndExpression(allExpression, oneExpress);
        }

        return allExpression;
    }

    /**
     * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中
     *
     * @param ms MappedStatement
     */
    private void addMappedStatementCache(MappedStatement ms) {
        if (ContextHolder.getRewrite()) {
            return;
        }
        // 无重写,进行添加
        mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules());
    }

    /**
     * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则
     *
     * @author yudao
     */
    static final class ContextHolder {

        /**
         * 该 {@link MappedStatement} 对应的规则
         */
        private static final ThreadLocal<List<DataPermissionRule>> RULES = ThreadLocal.withInitial(Collections::emptyList);
        /**
         * SQL 是否进行重写
         */
        private static final ThreadLocal<Boolean> REWRITE = ThreadLocal.withInitial(() -> Boolean.FALSE);

        public static void init(List<DataPermissionRule> rules) {
            RULES.set(rules);
            REWRITE.set(false);
        }

        public static void clear() {
            RULES.remove();
            REWRITE.remove();
        }

        public static boolean getRewrite() {
            return REWRITE.get();
        }

        public static void setRewrite(boolean rewrite) {
            REWRITE.set(rewrite);
        }

        public static List<DataPermissionRule> getRules() {
            return RULES.get();
        }

    }

    /**
     * {@link MappedStatement} 缓存
     * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效
     * 如果无效,则可以避免 SQL 的解析,加快速度
     *
     * @author yudao
     */
    static final class MappedStatementCache {

        /**
         * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存
         * <p>
         * value:{@link MappedStatement#getId()} 编号
         */
        private final Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = new ConcurrentHashMap<>();

        public Map<Class<? extends DataPermissionRule>, Set<String>> getNoRewritableMappedStatements() {
            return noRewritableMappedStatements;
        }

        /**
         * 判断是否无需重写
         * ps:虽然有点中文式英语,但是容易读懂即可
         *
         * @param ms    MappedStatement
         * @param rules 数据权限规则数组
         * @return 是否无需重写
         */
        public boolean noRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
            // 如果规则为空,说明无需重写
            if (org.springframework.util.CollectionUtils.isEmpty(rules)) {
                return true;
            }
            // 任一规则不在 noRewritableMap 中,则说明可能需要重写
            for (DataPermissionRule rule : rules) {
                Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
                if (mappedStatementIds != null && !mappedStatementIds.stream().anyMatch(item -> item.equals(ms.getId()))) {
                    return false;
                }
                return false;
            }
            return true;
        }

        /**
         * 添加无需重写的 MappedStatement
         *
         * @param ms    MappedStatement
         * @param rules 数据权限规则数组
         */
        public void addNoRewritable(MappedStatement ms, List<DataPermissionRule> rules) {
            for (DataPermissionRule rule : rules) {
                Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass());
                if (Objects.nonNull(mappedStatementIds) && CollectionUtils.isEmpty(mappedStatementIds)) {
                    mappedStatementIds.add(ms.getId());
                } else {
                    noRewritableMappedStatements.put(rule.getClass(), Arrays.stream(new String[]{ms.getId()}).collect(Collectors.toSet()));
                }
            }
        }

        /**
         * 清空缓存
         * 目前主要提供给单元测试
         */
        public void clear() {
            noRewritableMappedStatements.clear();
        }

    }

    /**
     * 获得 Table 对应的表名
     * <p>
     * 兼容 MySQL 转义表名 `t_xxx`
     *
     * @param table 表
     * @return 去除转移字符后的表名
     */
    public static String getTableName(Table table) {
        String tableName = table.getName();
        if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) {
            tableName = tableName.substring(1, tableName.length() - 1);
        }
        return tableName;
    }
}

2.8 DataPermissionRule.java

数据权限实现规则,业务系统也可以实现该接口,定义出自身的数据过滤规则;

package com.luo.chengrui.datapermission.rules;

import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;

import java.util.Set;

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

    /**
     * 获取需要权限过滤的表名,在sql拦截中先按表名进行匹配,匹配上再调用
     * @return
     */
    Set<String> getTableNames();

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

}

2.9 DefaultDataPermissionRule.java

插件实现的默认数据规则处理器,进行数据过滤的关键因素有两个:

  1. 哪些数据表、字段要做数据过滤;
  2. 拿到当前用户的数据权限;

以上两个问题不可能提前知道,只能是在业务使用时才知道。既然不知道,那么就把这两个问题交给业务系统或者说程序员去处理吧。
那么我们在实现默认数据规则处理器时,这样去解决上面两个问题:

  1. 将表和字段的配置提供一个方法addTableAndColumn(String tableName, String columnName)
  2. 把获取用户数据权限定义为一个接口,让用户创建默认规则对象时实现该接口。

综上,就有了默认数据规则处理器,如果就具备了处理任何表、任何权限的能力。

package com.luo.chengrui.datapermission.rules;

import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.NullValue;
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;

/**
 * 默认权限规则处理器
 */
public class DefaultDataPermissionRule<E extends Expression> implements DataPermissionRule {


    static final Expression EXPRESSION_NULL = new NullValue();


    protected final Map<String, String> tableAndColumns = new HashMap<>();
    /**
     * 所有表名,需要进行权限过滤的表名
     */
    protected final Set<String> TABLE_NAMES = new HashSet<>();

    /**
     * 获取权限范围的接口
     */
    private CustomerPermissionFunction permissionFunction;

    public DefaultDataPermissionRule(CustomerPermissionFunction permissionFunction) {
        this.permissionFunction = permissionFunction;
    }

    /**
     * 添加需要过滤的表和字段
     *
     * @param tableName  表名
     * @param columnName 字段名
     */
    public void addTableAndColumn(String tableName, String columnName) {
        tableAndColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }

	//定义了用户自定义权限方法,将具体权限获取交给业务去实现。从而让默认规则可以支持任何数据过滤规则。
    public interface CustomerPermissionFunction<E extends Expression> {
        Set<E> getPermission();
    }
    /**
     * 获取所有需要按部门权限过滤的表
     *
     * @return
     */
    @Override
    public Set<String> getTableNames() {
        return TABLE_NAMES;
    }

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        // 获取用户的权限
        Set<E> deptIds = permissionFunction.getPermission();
        if (deptIds == null) {
            //表示 不进行权限过滤,即有所有权限。
            return null;
        }
        //进行数据权限过滤 
        if (Objects.nonNull(tableAndColumns.get(tableName)) && deptIds != null && deptIds.size() > 0) {
            return new InExpression(buildColumn(tableName, tableAlias, tableAndColumns.get(tableName)),
                    new ExpressionList(deptIds.stream().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 + "." + column);
    }
}

综上,数据权限插件全部代码已经完成。

三、业务系统应用

3.1 pom.xml

如果本身是springboot+mybatis项目,那么仅需要引入:

  • mybatis-plus-boot-starter:mybatisplus依赖包
  • cr-datapermission:上面的数据权限插件
<?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>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.chengrui.lab</groupId>
    <artifactId>cr-lab-datapermission</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <sprint.boot.version>2.7.14</sprint.boot.version>
    </properties>

    <dependencies>

        <!-- 实现对数据库连接池的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我们使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <!-- 实现对 MyBatis 的自动化配置 -->
        <!--        <dependency>
                    <groupId>org.mybatis.spring.boot</groupId>
                    <artifactId>mybatis-spring-boot-starter</artifactId>
                    <version>2.1.1</version>
                </dependency>-->

        <!--        引入mybatisplus包,使用sqlpaser功能-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.2</version>
        </dependency>

<!--        我们的数据权限包-->
        <dependency>
            <groupId>com.chengrui.tool</groupId>
            <artifactId>cr-datapermission</artifactId>
            <version>1.0.1</version>
        </dependency>

        <!-- 方便等会写单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>
    </dependencies>


</project>

3.2 MybatisConfig.java——测试版

mybatis框架项目基本都有这个配置类,有这个配置类的时,需要把我们自定义的sql拦截器添加到mybatis拦截器队列中。
1创建规则对象2创建SQL拦截器对象3将拦截器添加到mybatis中

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
        String mapperLocations = env.getProperty("mybatis.mapperLocations");
        String configLocation = env.getProperty("mybatis.configLocation");
        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);

        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
	
		//1开始创建规则
		//用默认权限规则创建按部门过滤的
        DefaultDataPermissionRule dataPermissionRule = new DefaultDataPermissionRule(() -> {
        	//获取用户权限。这里获取最好从缓存里面获取,如果实时从数据库获取,会生一些怪事。比如和PageHelper一起使用时,获取权限的时候会被分页。
            Set<LongValue> deptId = new HashSet<>();
            deptId.add(new LongValue(4L));
            return deptId;
        });
        //注册需要按部门ID过滤的表和字段;
        dataPermissionRule.addTableAndColumn("users", "dept_id");
        List<DataPermissionRule> dataPermissionRules = new ArrayList<>();
        dataPermissionRules.add(dataPermissionRule);
        // 规则创建完成
		//2创建拦截器对象
        DataPermissionSqlParserInterceptor dataPermissionSqlParserInterceptor = new DataPermissionSqlParserInterceptor(dataPermissionRules);
        //3将拦截器添加到mybatis中。
        sessionFactory.setPlugins(dataPermissionSqlParserInterceptor);
        return sessionFactory.getObject();
    }

如果你的springboot版本支持自动配置,则注解拦截器你无需注册,如果不支持的话,还需手动注册一下。
运行结果:
在这里插入图片描述

3.3默认规则使用示例——正式版

在正式项目中,可能已经有MyBatisConfig类的配置,而且如果权限规则较多的情况下,可能不方便每次都修改MyBatisConfig类。那么也很好处理。
我在一个项目中的处理方式是单独创建一个配置类DefaultDataPermissionConfiguration.java.。

  • 首先创建了一个叫deptDataPermissionRule的DataPermissionRule 对象,如果有其他数据类型只需要再创建相关Bean即可,这里需要特别注意获取数据权限时最好从缓存中拿,否可能与PageHelper产生污染
  • 创建DataPermissionSqlParserInterceptor 对象方法,该方法需要两个参数:SqlSessionFactoryList<DataPermissionRule>;一方面创建拦截器,二是将拦截器注入sessionFactory中。
package com.ruoyi.web.core.config;

import cn.hutool.core.collection.CollectionUtil;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.luo.chengrui.datapermission.interceptor.DataPermissionSqlParserInterceptor;
import com.luo.chengrui.datapermission.rules.DataPermissionRule;
import com.luo.chengrui.datapermission.rules.DefaultDataPermissionRule;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.config.service.ICkStorageService;
import com.ruoyi.framework.datapermissionRule.DeptDatapermissionRule;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.StringValue;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
* @author
* @version 1.0.0
* @description
* @createTime 2023/07/17
*/
@Configuration
public class DefaultDataPermissionConfiguration {

   /**
    * 初始化部门权限 bean 。
    *
    * @return
    */
   @Bean
   public DataPermissionRule deptDataPermissionRule() {
       DefaultDataPermissionRule rule = new DefaultDataPermissionRule(() -> {
           // 如果是分页,这里需先去掉分页线参数,不然会出线线程污染。待数据权限查询结果完结后,再设置分页参数。
           // 当然更好的做法是权限数据从缓存中获取,这样就不需要如此做了。
           Page page = PageHelper.getLocalPage();
           PageHelper.clearPage();
           // 在这里动态获取用户权限;
           ICkStorageService ckStorageService = SpringUtils.getBean(ICkStorageService.class);
           Set<StringValue> set = null;
           //DeptDatapermissionRule是业务自己实现的权限获取方法。
           Set<Long> deptIds = DeptDatapermissionRule.getStorageIds();
           if (deptIds != null) {
               set = new HashSet<>();
               if (CollectionUtil.isNotEmpty(deptIds)) {
                   List<String> storageId = ckStorageService.getSotageIdsByDeptIds(deptIds);
                   set.addAll(storageId.stream().map(id -> new StringValue(id)).collect(Collectors.toList()));
               }
           }

           if (page != null) {
               // 把分页参数设置回线程变量中。
               PageHelper.startPage(page.getPageNum(), page.getPageSize(), page.getOrderBy()).setReasonable(page.getReasonable());
           }
           return set;
       });
       //当sql中包含以下两个表时,进行数据过滤。
       rule.addTableAndColumn("wms_order_info", "storage_id");
       rule.addTableAndColumn("wms_sorder_info", "storage_id");
       return rule;
   }

   @Bean
   public DataPermissionSqlParserInterceptor dataPermissionSqlParserInterceptor(SqlSessionFactory sqlSessionFactory, List<DataPermissionRule> dataPermissionRuleList) {
       DataPermissionSqlParserInterceptor dataPermissionSqlParserInterceptor = new DataPermissionSqlParserInterceptor(dataPermissionRuleList);
       if (sqlSessionFactory instanceof SqlSessionFactoryBean) {
           ((SqlSessionFactoryBean) sqlSessionFactory).setPlugins(dataPermissionSqlParserInterceptor);
       }
       return dataPermissionSqlParserInterceptor;
   }
}

springboot+mybatis数据权限管理插件
springboot+mybatisplus数据权限插件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1352007.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件工程期末总结

软件工程期末总结 软件危机出现的原因软件生命周期软件生命周期的概念生命周期的各个阶段 软件开发模型极限编程 可行性研究与项目开发计划需求分析结构化分析的方法结构化分析的图形工具软件设计的原则用户界面设计结构化软件设计面向对象面向对象建模 软件危机出现的原因 忽视…

Qt通过pos()获取坐标信息

背景&#xff1a;这是一个QWidget窗体&#xff0c;里面是各种布局的组合&#xff0c;一层套一层。 我希望得到绿色部分的坐标信息(x,y) QPoint get_pos(QWidget* w, QWidget* parent) {if ((QWidget*)w->parent() parent) {return w->pos();}else {QPoint pos(w->po…

【设计模式-2】原型模式的原理、代码实现及类图展示

我们一定对类的实例化比较熟悉&#xff0c;前面我们说的单例、还有3种工厂模式都是通过new关键字来创建对象&#xff0c;下面我们来了解一种新的对象创建的方式。 1. 定义 原型模式也是一种创建型的设计模式&#xff0c;实现和原理总体比较简单&#xff0c;一句话总结呢&#…

element-ui table-自定义表格某列的表头样式或者功能

自带表格 自定义表格某列的表头样式或者功能 <el-table><el-table-column :prop"date">//自定义表身每行数据<template slot-scope"scope">{{scope.row[scope.column.label] - ? - : scope.row[scope.column.label]}}</template>…

50套Threejs实现的Web3D学习案例,总有一套适合你

个人主页&#xff1a; 左本Web3D&#xff0c;更多案例预览请点击》 在线案例 个人简介&#xff1a;专注Web3D使用ThreeJS实现3D效果技巧和学习案例 &#x1f495; &#x1f495;积跬步以至千里&#xff0c;致敬每个爱学习的你。喜欢的话请三连&#xff0c;有问题请私信或者加微…

【华为机试】2023年真题B卷(python)-篮球比赛

一、题目 题目描述&#xff1a; 篮球(5V5)比赛中&#xff0c;每个球员拥有一个战斗力&#xff0c;每个队伍的所有球员战斗力之和为该队伍的总体战斗力。 现有10个球员准备分为两队进行训练赛&#xff0c;教练希望2个队伍的战斗力差值能够尽可能的小&#xff0c;以达到最佳训练效…

mysql聚簇索引和非聚簇索引

目录 InnoDB引擎MylSAM引擎聚簇索引的优点和缺点参考 聚簇索引和非聚簇索引的区别&#xff1a;叶节点是否存放一整行记录。 聚簇索引:将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据。 非聚簇索引:将数据与索引分开存储&#xff0c;索引结构的叶子节点指向了数据对…

SM2——适用于前后端(java+vue)公用的SM2国密加解密传输

目录 一、SM2国密加解密算法1.1、pom文件引入依赖包1.2、SM2加解密工具类1.3、测试类 一、SM2国密加解密算法 1.1、pom文件引入依赖包 <dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk18on</artifactId><version>…

android开发百度地图api实现定位图标随手机方向转动

该功能的实现依赖于手机中的传感器元件如陀螺仪、加速度计等&#xff0c;具体开发详见android的官方开发文档&#xff1a; 传感器概览 | Android 开发者 | Android Developershttps://developer.android.com/guide/topics/sensors/sensors_overview?hlzh-cn要自定义一个传…

从0开始搭建清华ChatGLM3 6b大模型(Windows RTX4090版)

目录 1、硬件及软件说明 2、安装Anaconda 3、安装Git版本控制 ​4、安装pytorch驱动 5、安装ChatGLM3 1、硬件及软件说明 硬件&#xff1a;主要是GPU卡内存要足够&#xff0c;本次搭建使用的RTX4090卡一张&#xff0c;单卡内存24G&#xff0c;为什么选择4090&#xff1f;…

前端map标签(创建热点区域或是点击图片指定区域跳转对应链接))

前言 点击整张图片的某一部分,可以实现自定义跳转或者一些事件 利用img和map和area标签实现 先来看下实现 https://www.w3cschool.cn/tryrun/showhtml/tryhtml_areamap <img src"/statics/images/course/planets.gif" width"145" height"126&…

aps审核-模电英文稿

模拟电子线路 Analog circuit 需要熟悉课程名&#xff0c;一句话简单概括课程内容&#xff0c;准备一些重点内容介绍。 This course mainly introduces the properties(n.性质) of semiconductors(半导体) and transistors, and then analyzes and masters amplification circ…

猜数字游戏

一. 游戏要求&#xff1a; 1. 电脑自动生成1~100的随机数 2. 玩家猜数字&#xff0c;猜数字的过程中&#xff0c;根据猜测数据的大小给出大了或小了的反馈&#xff0c;直到猜对&#xff0c;游戏结束。 二. 随机数的生成 2.1 rand C语言提供了⼀个函数叫rand&#xff0c;这函…

MS5148T荣获2023电子信息半导体行业年度卓越产品

MS5148T是一款适合高精度、低成本测量应用的24bit模数转换器。内部集成了低噪声可编程增益放大器、高精度Δ-Σ模数转换器和内部振荡器、低温漂基准和两路匹配的可编程电流源&#xff0c;以及传感器检测Burnout电流源和偏置电压产生器&#xff0c;支持四路差分输入。 主要特点…

Docker就应该这么学-01

第一章 容器与开发语言 1.1 Docker 最近一段时间&#xff0c;云计算领域最火的莫过于“容器”一词。提到容器&#xff0c;就不得不提 Docker,可以说 Docker 己经成为了容器的代名词。那么&#xff0c;什么是 Docker ? Docker 又能做什么呢&#xff1f;本章 我们就来简单介绍…

2023春季李宏毅机器学习笔记 02 :机器学习基本概念

资料 课程主页&#xff1a;https://speech.ee.ntu.edu.tw/~hylee/ml/2023-spring.phpGithub&#xff1a;https://github.com/Fafa-DL/Lhy_Machine_LearningB站课程&#xff1a;https://space.bilibili.com/253734135/channel/collectiondetail?sid2014800 一、機器學習基本原理…

MySQL将多条数据合并成一条的完整示例

数据库中存的是多条数据&#xff0c;展示的时候需要合并成一条 数据表存储形式如下图 以type分组&#xff0c;type相同的算一条&#xff0c;且保留image和link的所有数据&#xff0c;用groupBy只保留一条数据 解决方案&#xff1a;用GROUP_CONCAT 完整语法如下 group_concat…

基于人工智能的数据库工具Chat2DB使用

文章目录 前言Chat2DB介绍Chat2DB地址下载安装 Chat2DB配置Chat2DB使用1、自然语言转sql2. SQL解释3. SQL优化4. SQL转换 写在最后 前言 随着人工智能的发展&#xff0c;各行各业都出现了不少基于AI的工具来提升工作效率。就连国内的各个大厂也都在基于大模型开发自己的产品线…

ctfshow——信息搜集

文章目录 web 1web 2web 3web 4web 5web 6web 7web 8web 9web 10web 11web 12web 13web 14web 15web 16web 17web 18web 19web 20 web 1 题目提示开发注释未及时删除。 直接右键查看源代码。 web 2 在这关我们会发现&#xff1a;1&#xff09;无法使用右键查看源代码&…

Github 2023-12-31 开源项目日报 Top10

根据Github Trendings的统计&#xff0c;今日(2023-12-31统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量TypeScript项目3Swift项目1Java项目1HTML项目1Astro项目1Python项目1C项目1Dart项目1Jupyter Notebook项目1C项…