springboot+mybatis plus实现数据权限,在分页之前拦截sql进行修改

 拦截器

import cn.hutool.core.util.ReflectUtil;
import com.manager.enums.PositionStatusEnum;
import com.manager.util.LoginUserUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;

/**
 * mybatis 拦截顺序Executor -> StatementHandler->ParameterHandler->ResultSetHandler
 * 要在分页插件之前完成sql语句的修改 应拦截Executor
 * @author 
 */
@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}),
            })
public class DataAuthInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        //只拦截加注解的方法
        DataAuth dataAuth = this.getDataScope(ms);
        if (Objects.isNull(dataAuth)) {
            return invocation.proceed();
        }
        //只针对投资经理岗位做数据权限,如果多岗位则不控制
        List<Integer> positionStatus = LoginUserUtil.getPositionStatus();
        if (CollectionUtils.isEmpty(positionStatus) || !positionStatus.contains(PositionStatusEnum.PRODUCT_MANAGER.getCodeValue())
                || (positionStatus.contains(PositionStatusEnum.PRODUCT_MANAGER.getCodeValue()) && positionStatus.size() > 1)){
            return invocation.proceed();
        }
        if (StringUtils.isBlank(LoginUserUtil.getProductIds())){
            return invocation.proceed();
        }
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        //由于逻辑关系,只会进入一次
        if(args.length == 4){
            //4 个参数时
            //---------------这里就是拷贝的CachingExecutor或者BaseExecutor的代码
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            //6 个参数时
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        //-------------本来一般情况下是执行invocation.proceed(),继续执行拦截方法,但这里直接执行这个方法,相
        //-------------当于替换了CachingExecutor或者BaseExecutor原来的实现。
        String originalSql = boundSql.getSql();
        originalSql = this.modifyOrgSql(originalSql);
        BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), originalSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
//解决mybatis分页foreach 参数失效问题:There is no getter for property named ‘__frch_ _0’ in 'class
//得知新版需要用到additionalParameters参数,也就是说不论哪个版本,value都是从metaParameters获得,
//在老版本中使用metaParameters新版中使用了additionalParameters判断是否有值。而分页插件没有注入该参数,用ref加入该参数(注释的代码),问题解决
/* if (ReflectUtil.getFieldValue(boundSql,"additionalParameters") != null){
    Object metaParameters = ReflectUtil.getFieldValue(boundSql, "additionalParameters");
    ReflectUtil.setFieldValue(newBoundSql,"additionalParameters",metaParameters);
}*/
// todo ReflectUtil方法在有物理分页的情况下foreach #{item}可以正常取值,但是在没有物理分页的情况下foreach #{item}取值为null,故修改为以下方式
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
    String prop = mapping.getProperty();
    if (boundSql.hasAdditionalParameter(prop)) {
        newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
    }
}
        return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, newBoundSql);
    }

    /**
     * 通过反射获取mapper方法是否加了数据拦截注解
     */
    private DataAuth getDataScope(MappedStatement mappedStatement) throws ClassNotFoundException {
        DataAuth dataAuth = null;
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        final Class<?> cls = Class.forName(className);
        final Method[] methods = cls.getMethods();
        for (Method method : methods) {
            if (method.getName().equals(methodName) && method.isAnnotationPresent(DataAuth.class)) {
                dataAuth = method.getAnnotation(DataAuth.class);
                break;
            }
        }
        return dataAuth;
    }

    /**
     * 根据权限点拼装对应sql
     * @return 拼装后的sql
     */
    private String modifyOrgSql(String originalSql){
        return  "select * from (" + originalSql + ") temp_data_scope where temp_data_scope.productId in (".concat(LoginUserUtil.getProductIds()).concat(")");
    }

}

LoginUserUtil是一个线程对象,本次采用的方案是将所有的权限数据在验证token的拦截器处理好,并放入线程对象中的属性productIds,在该自定义拦截器中只需要去来直接用即可。具体的权限逻辑根据自己业务实现

自定义注解

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

/**
 * 产品权限注解
 * @author 
 */
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataAuth {

}

配置自定义拦截器

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.manager.productrole.DataAuthInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan({"com.manager.dao","com.manager.module.**.dao"})
public class MybatisPlusConfig {

    /**
     * 分页
     **/
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

    @Bean
    ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return configuration -> {
            configuration.addInterceptor(new com.github.pagehelper.PageInterceptor());
            configuration.setMapUnderscoreToCamelCase(false);
        };
    }

    //由于拦截器加载顺序1>2>3执行顺序就会变成3>2>1,所以自定义拦截器应当放在分页拦截器之后加载
    @Bean
    public DataAuthInterceptor dataAuthInterceptor(){
        return new DataAuthInterceptor();
    }

自定义注解的使用

自定义注解使用在mapper的方法上

public interface ProductDocumentMapper extends BaseMapper<ProductDocument> {
    @DataAuth
    List<ProductDocumentListRsp> getList(ProductDocumentListReq productDocumentListReq);

}

总结:期间遇到这个报错:There is no getter for property named ‘__frch_ _0’ in 'class

解决方案:这个报错的问题是由于mybatis foreach标签使用报错
#{item}这个写法本身没问题,由于分页插件的问题

可以改成:${item}  //有sql注入的风险,不推荐

                #{list[${index}]}

最终解决方案:

修改代码费时费力,还容易出错,最终解决方案

if (ReflectUtil.getFieldValue(boundSql,"additionalParameters") != null){
    Object metaParameters = ReflectUtil.getFieldValue(boundSql, "additionalParameters");
    ReflectUtil.setFieldValue(newBoundSql,"additionalParameters",metaParameters);
}

参考:

如何在PageHelper之前拦截sql_a361117441的博客-CSDN博客_sql拦截

There is no getter for property named ‘__frch_item_0‘ in ‘class_bbq烤鸡的博客-CSDN博客

MyBatis 物理分页foreach 参数失效(list值传不进<foreach>标签为null)_jbgtwang的博客-CSDN博客_foreach mybatis null

Mybatis的SQL语句拦截及修改_luxc666的博客-CSDN博客_mybatis拦截sql语句


版权声明:本文为qq_38377774原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。