自定义 Mybatis 拦截器实现权限功能

目录

1、Mybatis 拦截器介绍

1.1 Mybatis 执行流程

1.2 Mybatis中可以被拦截的类型

1.3 使用规则

1.4 拦截器重写的方法

2、实战部分:拦截实现


1、Mybatis 拦截器介绍

1.1 Mybatis 执行流程

  • 首先读取配置文件,然后加载映射文件,由SqlSessionFactory工厂对象去创建核心对象SqlSession,SqlSession对象会通过Executor执行器对象执行sql。然后Executor执行器对象会调用StatementHandler对象去真正的访问数据库执行sql语句。
  • 在执行sql语句前MapperStatement会先对映射信息进行封装,然后StatementHandler调用ParameterHandler去设置编译参数【#{},${}】,编译在StatementHandler中进行。然后StatementHandler调用JBDC原生API进行处理,获取执行结果,这个执行结果交给ResultSetHandler 来进行结果集封装,然后将结果返回给StatementHandler。
  • 注意: 这里MapperStatement是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。TypeHandler进行数据库类型和JavaBean类型映射处理。

1.2 Mybatis中可以被拦截的类型

  • Executor:拦截执行器的方法。
  • ParameterHandler:拦截参数的处理。
  • ResultHandler:拦截结果集的处理。
  • StatementHandler:拦截Sql语法构建的处理。

1.3 使用规则

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
    /**
     * 定义拦截点
     * 只有符合拦截点的条件才会进入到拦截器
     */
    Signature[] value();
}

Signature来指定咱们需要拦截那个类对象的哪个方法

- type:上述四种类型中的一种;
- method:对应接口中的哪类方法(因为可能存在重载方法);
- args:对应哪一个方法的入参;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  /**
   * 定义拦截的类 Executor、ParameterHandler、StatementHandler、ResultSetHandler当中的一个
   */
  Class<?> type();

  /**
   * 在定义拦截类的基础之上,在定义拦截的方法
   */
  String method();

  /**
   * 在定义拦截方法的基础之上在定义拦截的方法对应的参数,
   * JAVA里面方法可能重载,故注意参数的类型和顺序
   */
  Class<?>[] args();
}

1.4 拦截器重写的方法

public interface Interceptor {
    //起拦截作用,在此定义一些功能
    Object intercept(Invocation var1) throws Throwable;

    //这个方法的作用是就是让mybatis判断,是否要进行拦截,然后做出决定是否生成一个代理
    Object plugin(Object var1);

    //拦截器需要一些变量对象,而且这个对象是支持可配置的。
    void setProperties(Properties var1);
}

2、实战部分:拦截实现

自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DepartAuth {
    /**
     * 添加查询条件的字段名
     * @return
     */
    String field();

    EnumDepartAuthType authType() default EnumDepartAuthType.DEPART_ID;
    
}

在所在接口上添加注解

@DepartAuth(field = "xxx", authType = )

切面(AuthAspect)

@Slf4j
@Aspect
@Component
public class DepartAuthAspect {

    @Autowired
    private DepartAuthHandler departAuthHandler;

    @Pointcut("@annotation(org.jeecg.common.auth.depart.annotation.DepartAuth)")
    public void departAuthPoint() {

    }

    @Before("departAuthPoint()")
    public void before(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

        Annotation[] annotations = methodSignature.getMethod().getAnnotations();

        for (Annotation annotation : annotations) {
            if (annotation instanceof DepartAuth) {
                String field = ((DepartAuth) annotation).field();
                departAuthHandler.beforeHandler((DepartAuth) annotation);
            }
        }

    }

    @After("departAuthPoint()")
    public void after(JoinPoint pjp) {
        departAuthHandler.afterHandler();
    }
}

DepartAuthHandler

将信息存储到 TreadLocalhost 中,方便后续修改sql的时候可以读取到需要修改的字段

@Slf4j
@Component
public class DepartAuthHandler {

    public static ThreadLocal<DepartAuth> DEPART_AUTH_CACHE = new ThreadLocal<>();
    public static ThreadLocal<Integer> DEPART_AUTH_COUNT = new ThreadLocal<>();

    public void beforeHandler(DepartAuth departAuth) {
        String field = departAuth.field();
        if(StringUtils.isNotBlank(field)) {
            DEPART_AUTH_CACHE.set(departAuth);
            DEPART_AUTH_COUNT.remove();
        }
        PriorityQueue queue = new PriorityQueue<>();
        queue.peek();
    }

    public void afterHandler() {
        DEPART_AUTH_CACHE.remove();
        DEPART_AUTH_COUNT.remove();
    }
}

拦截器部分

  1. 获取StatementHandler:通过 invocation.getTarget() 获取当前被拦截的 StatementHandler 对象。由于 MyBatis 使用了代理模式,因此这里得到的是一个代理对象。接着,通过反射获取其实际的代理对象 ,即最终执行SQL的 StatementHandler 实例。
  2. 获取MappedStatement:通过反射从 StatementHandler 中获取 mappedStatement 对象。这个对象包含了关于即将执行的SQL语句的所有信息,包括SQL类型、参数类型等。
  3. 判断SQL类型:通过mappedStatement.getSqlCommandType()获取SQL命令类型。如果类型是SELECT,说明当前是一个查询操作,需要进行权限检查和处理。
  4. 获取并解析原始SQL:通过delegate.getBoundSql()获取BoundSql对象,它包含了实际执行的SQL语句和相关的参数信息。然后使用CCJSqlParserUtil.parse()解析这个SQL语句,得到一个抽象语法树(AST)。
  5. 修改SQL:从AST中提取出PlainSelect对象(即SELECT语句的主体)。然后调用自定义的buildWhereClause方法,根据departAuth中的权限信息构建一个权限检查条件,并将其注入到原始的SELECT语句中。这通常是通过在WHERE子句后追加额外的条件来实现的。
  6. 更新BoundSql对象:将修改后的SQL语句重新设置回BoundSql对象中,以便MyBatis在执行时能够使用修改后的SQL。
  7. 继续执行后续流程:在完成SQL修改后,调用invocation.proceed()继续执行MyBatis的后续处理流程,包括实际的SQL执行、结果集处理等。
@Data
@Slf4j
@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class DepartAuthMapperInterceptor implements Interceptor {

    private Properties properties;




    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        DepartAuth departAuth = DepartAuthHandler.DEPART_AUTH_CACHE.get();
        Integer count = DepartAuthHandler.DEPART_AUTH_COUNT.get();
        if(departAuth != null && count == null) {
            // 说明当前线程已经执行了过滤条件,避免递归调用
            DepartAuthHandler.DEPART_AUTH_COUNT.set(1);
            RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
            //获取StatementHandler构造器
            StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
            // 通过反射获取delegate父类BaseStatementHandler的mappedStatement属性
            MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
            SqlCommandType commandType = mappedStatement.getSqlCommandType();
            // 处理select对象
            if (SqlCommandType.SELECT.equals(commandType)) {
                // 获取原始sql
                BoundSql boundSql = delegate.getBoundSql();
                Statement statement = CCJSqlParserUtil.parse(boundSql.getSql());
                PlainSelect selectBody = (PlainSelect) ((Select) statement).getSelectBody();
                log.info("原始 sql:{}", boundSql.getSql());
                // 拼接新条件
                buildWhereClause(selectBody, getSql(departAuth));
                ReflectUtil.setFieldValue(boundSql, "sql", statement.toString());
            }
            return invocation.proceed();

        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    /**
     * 添加查询条件
     * @param select
     * @param dataFilter
     * @throws JSQLParserException
     */
    private void buildWhereClause(PlainSelect select, String dataFilter) throws JSQLParserException {
        if(StringUtils.isBlank(dataFilter)) {
            return;
        }
        if (select.getWhere() == null) {
            select.setWhere(CCJSqlParserUtil.parseCondExpression(dataFilter));
        } else {
            AndExpression and = new AndExpression(
                    CCJSqlParserUtil.parseCondExpression(dataFilter), select.getWhere());
            select.setWhere(and);
        }
    }

    private String getSql(DepartAuth departAuth) {
        //结合自己的业务,拼接相对应的sql语句
    }
}

  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值