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

本文介绍了如何在SpringBoot应用中结合Mybatis Plus实现数据权限控制。通过拦截器处理Token验证,并将权限数据放入线程对象。详细讨论了自定义注解的创建、拦截器配置以及注解的使用方法。在实际操作中遇到了关于Mybatis分页插件的`foreach`标签报错问题,解决方案包括避免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的博客-优快云博客_sql拦截

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

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

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

需求追加

现在某个接口需要支持两种情况,一种需要权限过滤,一种不需要
 

    在入参对象:ProductDocumentListReq 中添加新的参数:

    /**
     * 是否开启 数据权限过滤  "true"开启 ,"false"关闭,不传参默认开启
     */
    private String isEnableDataAuth;

同时DataAuth自定注解做修改:

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataAuth {

    //是否开启数据权限,默认开启
    String value() default "true";

}


新增spel表达式解析工具类:

package com.manager.productrole;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.Map;

/**
 * SpEL表达式解析工具类
 *
 */
@Slf4j
public class SpelExpressionResolver {
    
    private static final ExpressionParser parser = new SpelExpressionParser();

    /**
     * 新增:解析SpEL表达式判断是否开启数据权限
     */
    public static boolean evaluateSpElExpression(String expression, Object parameterObject) {
        try {
            StandardEvaluationContext context = new StandardEvaluationContext();

            if (parameterObject != null) {
                // 支持Map类型参数(多参数)
                if (parameterObject instanceof Map) {
                    ((Map<?, ?>) parameterObject).forEach((key, value) -> {
                        if (key instanceof String) {
                            context.setVariable((String) key, value);
                        }
                    });
                } else {
                    // 单参数情况,使用类名首字母小写作为变量名
                    String variableName = getVariableName(parameterObject.getClass());
                    context.setVariable(variableName, parameterObject);
                    context.setRootObject(parameterObject);
                }
            }

            Expression expr = parser.parseExpression(expression);
            Object result = expr.getValue(context);

            // 将结果转换为boolean
            if (result instanceof Boolean) {
                return (Boolean) result;
            } else if (result instanceof String) {
                return !StringUtils.equalsIgnoreCase(String.valueOf(result), "false");
            }

            // 如果解析结果不是boolean类型,默认开启数据权限(保持原有行为)
            return true;

        } catch (Exception e) {
            // 解析失败时,默认开启数据权限(保持原有行为)
            log.error("SpEL表达式解析失败: {} ,默认开启数据权限 ",expression,e);
            return true;
        }
    }

    /**
     * 新增:获取变量名(类名首字母小写)
     */
    private static String getVariableName(Class<?> clazz) {
        String className = clazz.getSimpleName();
        if (className.length() > 1) {
            return Character.toLowerCase(className.charAt(0)) + className.substring(1);
        }
        return className.toLowerCase();
    }

}


优化后的拦截器:

import com.manager.util.LoginUserUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.rpc.RpcContext;
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.mapping.ParameterMapping;
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 org.slf4j.MDC;

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

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

        // 新增:如果是SpEL表达式,则解析表达式判断是否开启数据权限
        if (dataAuth.value().startsWith("#")) {
            boolean enableDataAuth = SpelExpressionResolver.evaluateSpElExpression(dataAuth.value(), args[1]);
            if (!enableDataAuth) {
                return invocation.proceed();
            }
        }

        String allProductAuth = RpcContext.getContext().getAttachment("allProductAuth");
        String allProductAuth2 = MDC.get("allProductAuth");
        if(StringUtils.isNotEmpty(allProductAuth) || StringUtils.isNotEmpty(allProductAuth2)) {
            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加入该参数(注释的代码),问题解决
        // 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(")");
    }
}


mapper上的直接使用修改:

    @DataAuth("#productListReq.isEnableDataAuth")
    List<ProductListRsp> queryList(ProductListReq productListReq);

 使用spel表达式要确保项目中已经添加了mvn依赖

<!-- Maven -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-expression</artifactId>
    <version>5.3.xx</version>
</dependency>

<!-- 或者Gradle -->
implementation 'org.springframework:spring-expression:5.3.xx'

 
关于自定义注解里面 的参数:String value()而不是boolean  value()

类型优点缺点
String✅ 支持SpEL表达式
✅ 支持动态控制
✅ 灵活性高
✅ 向后兼容
❌ 类型不够严格
boolean✅ 类型安全
✅ 编译期检查
❌ 无法支持SpEL
❌ 只能静态控制
❌ 破坏现有功能

内容概要:本文介绍了一个关于超声谐波成像中幅度调制聚焦超声所引起全场位移和应变的分析模型,并提供了基于Matlab的代码实现。该模型旨在精确模拟和分析在超声谐波成像过程中,由于幅度调制聚焦超声作用于生物组织时产生的力学效应,包括全场的位移与应变分布,从而为医学成像和治疗提供理论支持和技术超声谐波成像中幅度调制聚焦超声引起的全场位移和应变的分析模型(Matlab代码实现)手段。文中详细阐述了模型构建的物理基础、数学推导过程以及Matlab仿真流程,具有较强的理论深度与工程应用价值。; 适合人群:具备一定声学、生物医学工程或力学背景,熟悉Matlab编程,从事医学成像、超声技术或相关领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①用于超声弹性成像中的力学建模与仿真分析;②支持高强度聚焦超声(HIFU)治疗中的组织响应预测;③作为教学案例帮助理解超声与组织相互作用的物理机制;④为相关科研项目提供可复用的Matlab代码框架。; 阅读建议:建议读者结合超声物理和连续介质力学基础知识进行学习,重点关注模型假设、偏微分方程的数值求解方法及Matlab实现细节,建议动手运行并修改代码以加深理解,同时可拓展应用于其他超声成像或治疗场景的仿真研究。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

《小书生》

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值