基于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注入等相关问题。如果文章存在问题欢迎各位大佬指正。

相关推荐
栗豆包8 分钟前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven
万亿少女的梦16821 分钟前
基于Spring Boot的网络购物商城的设计与实现
java·spring boot·后端
开心工作室_kaic2 小时前
springboot485基于springboot的宠物健康顾问系统(论文+源码)_kaic
spring boot·后端·宠物
0zxm2 小时前
08 Django - Django媒体文件&静态文件&文件上传
数据库·后端·python·django·sqlite
秋恬意9 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
刘大辉在路上9 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
追逐时光者11 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~11 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱5813612 小时前
InnoDB 的页分裂和页合并
数据库·后端