基于Mybatis拦截器实现数据权限过滤

背景

现在的项目负责人去年年底离职,导致前期规划的数据过滤功能一直没有去实现。现在项目马上进入试运行时期了,需要根据用户数据权限的配置对数据进行过滤处理。如果一个个手动去Mapper.xml文件中修改SQL工作量太大了,后面我考虑通过Mybatis对查询的SQL进行处理。

基础知识

Mybatis 拦截器介绍

Interceptor接口源码解析

java 复制代码
package org.apache.ibatis.plugin;  
  
import java.util.Properties;  
  
public interface Interceptor {  
	
    Object intercept(Invocation invocation) throws Throwable;  

    default Object plugin(Object target) {  
        return Plugin.wrap(target, this);  
    }  
    
    default void setProperties(Properties properties) {  
    }  
}
  • intercept 方法 这个方法是核心,当拦截到调用时会执行。Invocation 对象包含了被拦截方法的所有信息,包括方法本身、参数、目标对象等。在这个方法中,你可以做任何预处理或后处理逻辑,然后通过调用 invocation.proceed() 来继续执行原方法,或者直接返回自定义的结果。
  • plugin方法 这个方法用于决定是否对某个对象应用拦截器。如果返回 target,则表示不进行拦截;如果返回一个新的对象,则表示将使用这个新对象替代原有的对象,通常是在这里返回一个代理对象。
  • setProperties 方法 用于设置拦截器的属性,这些属性可以在 MyBatis 的配置文件中定义。

Signature 注解源码解析

java 复制代码
@Documented  
@Retention(RetentionPolicy.RUNTIME)  
@Target({})  
public @interface Signature {  
	Class<?> type();

	String method();

	Class<?>[] args();
}
  • type:表示目标对象的类型,
  • method:表示要拦截的目标方法的名字。
  • args:表示目标方法的参数类型列表。不同的 @Signature 注解可能有不同的参数类型列表,这取决于具体的方法签名。

代码实战

实现一个类似与PageHelper的一个工具类,在本地线程变量中存储数据权限相关信息

java 复制代码
public class DataAccessMethod {  
    private static final ThreadLocal<DataAccessType[]> ACCESS_LOCAL = new ThreadLocal<>();  
  
  
    public DataAccessMethod() {  
  
    }  
    public static void setLocalAccess(DataAccessType... accessType) {  
        ACCESS_LOCAL.set(accessType);  
    }  
  
    public static DataAccessType[] getLocalAccess() {  
        return ACCESS_LOCAL.get();  
    }  
  
    public static void clearLocalAccess() {  
        ACCESS_LOCAL.remove();  
    }  
  
    public static void accessData(DataAccessType... accessType) {  
        setLocalAccess(accessType);  
    }  
}

实现 Interceptor接口对SQL进行增强处理

java 复制代码
@Intercepts({@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}  
)})  
@Slf4j  
@Component  
public class DataAccessFilterInterceptor implements Interceptor {  
  
  
    @Override  
    public Object intercept(Invocation invocation) throws Throwable {  
        if (skip()) {  
            return invocation.proceed();  
        }  
        MappedStatement statement = (MappedStatement) invocation.getArgs()[0];  
        Object parameter = invocation.getArgs()[1];  
        BoundSql boundSql = statement.getBoundSql(parameter);  
        String originalSql = boundSql.getSql();  
        Object parameterObject = boundSql.getParameterObject();  
        String sql = addTenantCondition(originalSql, "1", "222");  
        log.info("原SQL:{}, 数据权限替换后的SQL:{}", originalSql, sql);  
  
        BoundSql newBoundSql = new BoundSql(statement.getConfiguration(), sql, boundSql.getParameterMappings(), parameterObject);  
        MappedStatement newStatement = copyFromMappedStatement(statement, new BoundSqlSqlSource(newBoundSql));  
        invocation.getArgs()[0] = newStatement;  
        return invocation.proceed();  
    }  
  
    /**  
     * 判断是否跳过  
     *  
     * @return 是否跳过  
     */  
    private boolean skip() {  
        DataAccessType[] localAccess = DataAccessMethod.getLocalAccess();  
        return localAccess == null;  
    }  
  
    private String addTenantCondition(String originalSql, String depId, String alias) {  
        String field = "id";  
        if (StringUtils.hasText(alias)) {  
            field = alias + "." + field;  
        }  
  
        StringBuilder sb = new StringBuilder(originalSql.toLowerCase());  
        int index = sb.indexOf("where");  
        sb = new StringBuilder(originalSql);  
        if (index < 0) {  
            sb.append(" where ").append(field).append(" = ").append(depId);  
        } else {  
            sb.insert(index + 5, " " + field + " = " + depId + " and ");  
        }  
        return sb.toString();  
    }  
  
    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {  
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());  
        builder.resource(ms.getResource());  
        builder.fetchSize(ms.getFetchSize());  
        builder.statementType(ms.getStatementType());  
        builder.keyGenerator(ms.getKeyGenerator());  
        builder.timeout(ms.getTimeout());  
        builder.parameterMap(ms.getParameterMap());  
        builder.resultMaps(ms.getResultMaps());  
        builder.cache(ms.getCache());  
        builder.useCache(ms.isUseCache());  
        return builder.build();  
    }  
  
    public static class BoundSqlSqlSource implements SqlSource {  
        private final BoundSql boundSql;  
  
        public BoundSqlSqlSource(BoundSql boundSql) {  
            this.boundSql = boundSql;  
        }  
  
        @Override  
        public BoundSql getBoundSql(Object parameterObject) {  
            return boundSql;  
        }  
    }  
  
  
}

总结

以上代码只是示例,在实际生产中还需要考虑多表查询、SQL注入等相关问题。如果文章存在问题欢迎各位大佬指正。

相关推荐
2401_895521344 小时前
SpringBoot Maven快速上手
spring boot·后端·maven
disgare4 小时前
关于 spring 工程中添加 traceID 实践
java·后端·spring
ictI CABL4 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
小江的记录本6 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
lclcooky7 小时前
Spring 中使用Mybatis,超详细
spring·tomcat·mybatis
椰汁菠萝7 小时前
Mybatis-plus + PostgreSQL json格式类型转换异常
postgresql·json·mybatis
yhole9 小时前
springboot三层架构详细讲解
spring boot·后端·架构
香香甜甜的辣椒炒肉9 小时前
Spring(1)基本概念+开发的基本步骤
java·后端·spring
白毛大侠10 小时前
Go Goroutine 与用户态是进程级
开发语言·后端·golang
ForteScarlet10 小时前
从 Kotlin 编译器 API 的变化开始: 2.3.20
android·开发语言·后端·ios·开源·kotlin