掘金 后端 ( ) • 2024-04-10 10:27

image.png

引言

Mybatis Plus对Mybatis做了无侵入的增强,非常的好用,今天就给大家介绍它的其中一个实用功能:数据权限插件。

数据权限插件的应用场景和多租户的动态拦截拼接SQL一样。建议点赞+收藏+关注,方便以后复习查阅。

image.png

依赖

首先导入Mybatis Plus的maven依赖,我使用的是3.5.3.2版本。

<properties>
    <mybatis-plus.version>3.5.3.2</mybatis-plus.version>
</properties>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>

数据权限拦截器

写一个自定义的权限注解,该注解用来标注被拦截方法,注解上可以配置数据权限的表别名和表字段,它们会在拼接sql的时候用到。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyDataScope {
    /**
     * 表别名设置
     */
    String alias() default "";
    
    /**
     * 数据权限表字段名
     */
    String dataId() default "";
}

接下来就是写最核心的拦截器的处理逻辑了。创建一个接口实现类,实现Mybatis Plus的DataPermissionHandler接口。DataPermissionHandler的接口方法getSqlSegment有两个参数。

  • Expression where。where参数是mapper接口在xml中定义的sql的where条件表达式,在拦截处理器中我们可以给where条件表达式添加一些 and 或 or 的条件。
  • String mappedStatementId。mappedStatementId参数是mapper接口方法的全限定名,通过它我们可以得到mapper接口的Class类名以及接口方法名。

DataPermissionHandler的接口方法getSqlSegment会返回一个Expression类型的结果,即通过拦截器方法我们将原始的where条件表达式做了修改之后返回给Mybatis Plus并在代码运行时生效。

在拦截器方法中还使用到了一开始我们自定义的MyDataScope注解,没有被MyDataScope注解标注过的mapper方法我们直接返回原始的where条件表达式即可。

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.itguoguo.annotation.MyDataScope;
import com.itguoguo.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;

import java.lang.reflect.Method;
import java.util.Objects;

import static com.itguoguo.utils.LoginUserUtils.getLoginUser;

@Slf4j
public class MyDataScopeHandler implements DataPermissionHandler {
    /**
     * 获取数据权限 SQL 片段表达式
     * @param where             待执行 SQL Where 条件表达式
     * @param mappedStatementId Mybatis MappedStatement Id 根据该参数可以判断具体执行方法
     * @return 数据权限 SQL 片段表达式
     */
    @Override
    public Expression getSqlSegment(Expression where, String mappedStatementId) {
        try {
            String className = mappedStatementId.substring(0, mappedStatementId.lastIndexOf("."));
            String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
            Method[] methods = Class.forName(className).getMethods();
            for (Method m : methods) {
                if (StrUtil.isBlank(m.getName()) || !m.getName().equals(methodName)) {
                    continue;
                }
                MyDataScope annotation = m.getAnnotation(MyDataScope.class);
                if (Objects.isNull(annotation)) {
                    return where;
                }
                String sqlSegment = getSqlSegment(annotation);
                return StrUtil.isBlank(sqlSegment) ? where : getExpression(where, sqlSegment);
            }
        } catch (ClassNotFoundException e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * 拼接需要在业务 SQL 中额外追加的数据权限 SQL
     * @param annotation
     * @return 数据权限 SQL
     */
    private String getSqlSegment(MyDataScope annotation) {
        LoginUser loginUser = getLoginUser();
        String userType = loginUser.getSysUser().getUserType();
        Long userId = loginUser.getSysUser().getUserId();
        String sqlSegment = "";
        if (StrUtil.isBlank(userType)) {
            return sqlSegment;
        }
        if ("0".equals(userType)) {
            return sqlSegment;
        } else {
            sqlSegment = StrUtil.format(" {}.{} IN (SELECT project_id FROM sys_user where user_id = '{}') ",
                    annotation.alias(), annotation.dataId(), userId);
        }
        return sqlSegment;
    }

    /**
     * 将数据权限 SQL 语句追加到数据权限 SQL 片段表达式里
     * @param where         待执行 SQL Where 条件表达式
     * @param sqlSegment    数据权限 SQL 片段
     * @return
     */
    private Expression getExpression(Expression where, String sqlSegment) {
        try {
            Expression sqlSegmentExpression = CCJSqlParserUtil.parseCondExpression(sqlSegment);
            return (null != where) ? new AndExpression(where, sqlSegmentExpression) : sqlSegmentExpression;
        } catch (JSQLParserException e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }
}

拦截器配置

配置Mybatis Plus拦截器,数据权限handler作为参数传给拦截器构造方法。

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataScopeHandler()));
        return interceptor;
    }
}

使用

使用时,在mapper接口的方法上标注MyDataScope注解,给注解标上表别名和表字段。

public interface MyMapper {
    @MyDataScope(alias = "a", dataId = "id")
    List findList();
}

建议点赞+收藏+关注,方便以后复习查阅。

image.png