当前位置: 首页 > article >正文

Mybatis-plus拦截器BaseMultiTableInnerInterceptor实现(使用场景)

Mybatis-plus拦截器BaseMultiTableInnerInterceptor数据权限(使用场景)

前言

​ 前段时间接到需求,某某业务系统上线一年有余 ,但是目前未作数据隔离,所有账号看到的数据都是一样的,系统设计初期如此,现在组织变更存在多个分子公司,组织树变壮大,需要做数据隔离;让不同的人员不同的角色通过设置权限看到的数据不一样,考虑到目前系统已经上线,此次改造还需要慎重考虑。

​ 由于系统上线已久,业务接口颇多,不可能通过代码修改每个接口对数据进行过滤操作数据,工作量太大了,只能在最终执行的SQL语句处进行拦截,而如果每个语句都特殊处理的话也是个巨大的工程,之前刚好接触过一点Mybatis-plus拦截器的使用,此次改造主要用此拦截器结合其他技术实现,应用场景和多租户的动态拦截拼接SQL一样,简单来说就是在最终执行的SQL上在拼接上需要增加的数据范围的SQL语句;

​ 此处使用BaseMultiTableInnerInterceptor主要是因为报表等模块复杂SQL问题,若只是简单的SQL拦截处理,使用继承JsqlParserSupport也可用,后续有时间也会写此方法的使用情况(如租户隔离);

​ BaseMultiTableInnerInterceptor对mybatis_plus版本要求较高(3.5.3.1);

方案设计

  • 目前此系统的数据范围权限是人员所属的角色可控制各个模块能看到的数据范围(如图),本质是存储的就是对应人员所属的角色的相关的组织机构id;如设置本分子公司及其以下就是存储的这个这个分子公司下的所有组织机构id;所以后期拦截器就是为了把这些返回的orgId拼接在最终执行的SQL语句后再去执行;

    //改造前sql语句
    select * from material_dictionary  t1 left join material_category t2 on  t1.category_id =t2.id  where t1.tenant_id ='1111';
    //改造后拦截后sql语句  
    select * from material_dictionary  t1 left join material_category t2 on  t1.category_id =t2.id where t1.tenant_id ='1111'
    and  t1.orgid in('1','2','3') and t2.orgid in('1','2','3')   ;
    

    在这里插入图片描述

  • 此数据范围是平台的公告能力,但是业务系统是基于平台开发的业务模块,业务模块之前没使用到平台的数据范围数据;

  • 前期考虑问题

    • Mybatis-plus拦截器会拦截所有的SQL语句?那是否需要所有的都拦截拼接数据范围的语句呢?那些需要放行,那些需要拦截 ??

    • 获取查询以上平台的数据范围返回的数据(获取时机问题需要考虑,不能等sql执行的时候一直等着去查询平台的数据范围,导致整个查询变慢);

    • 获取平台数据范围是通过redis缓存下 ,还是http请求时就拦截缓存到某处(线程局部变量ThreadLocal);

      带着这些问题,开始设计了…

  • 设计思路

    • 所有业务表增加相关数据范围字段;

    • http拦截器拦截前端所有的http请求(业务接口)

    • 获取此次http请求模块平台对应的用户数据范围,存线程局部变量ThreadLocal;

    • 业务接口到达mybatis_plus数据范围拦截器拦截,加载ThreadLocal中的用户数据范围,构建动态sql(拼接sql);

    • 层层返回结果;

      注:以上只是理想化的使用场景,实际中还有未考虑到的场景,先设计再二次改造;

  • 设计图
    在这里插入图片描述

实现过程

  • 数据库表增加数据权限表字段

    • 名称编码类型说明
      租户idtenant_idvarchar(32)当前数据归属租户ID
      组织idorg_idvarchar(32)创建当前数据的组织ID
      组织名称org_namevarchar(128)创建当前数据的组织名称
      分子公司idcompany_idvarchar(32)分子公司ID,取orgId上级分子公司
      分子公司名称company_namevarchar(128)分子公司名称,取orgId上级分子公司
      创建人create_user_idvarchar(36)创建人
      ----------- 批量生成所有表需要增加的字段语句----
      SELECT CONCAT('ALTER TABLE ', table_name, 
       'ADD COLUMN orgId TO org_id,
       ADD COLUMN orgName TO org_name,
        ADD COLUMN companyId TO company_id ,
        ADD COLUMN companyName TO company_name ,
        ADD COLUMN createUserId TO create_user_id, 
      ') 
       FROM information_schema.tables
       WHERE table_schema = 'zcgl_dev';   -- 数据库名
      

      如图所示,导出粘贴出来直接执行
      在这里插入图片描述

  • 字段增加索引

      --给所有表生成索引字段如下
       select CONCAT(
           'ALTER TABLE ', table_name,
           ' ADD INDEX idx_', '_org_id (org_id); ',
           'ALTER TABLE ', table_name,
           ' ADD INDEX idx_', '_company_id (company_id) ;'
       ) from (
       SELECT 
         DISTINCT table_name
       FROM information_schema.columns
       WHERE 
         table_schema = DATABASE()
         AND table_name NOT IN ('temp_tables')
       ) t;
    
  • web请求拦截器

    配置web端请求拦截器,拦截所有的web请求,获取数据范围信息存,

    
    import lombok.extern.slf4j.Slf4j;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Arrays;
    
    /**
     * 数据权限上下文拦截器
     */
    @Slf4j
    public class DataPermissionRequestInterceptor implements HandlerInterceptor {
        private final static Logger logger = LoggerFactory.getLogger(DataPermissionRequestInterceptor.class);
        //数据范围过滤
        private final static String DATA_SCOPE_KEY = "dataScope";
        @Resource
        private DataScopeManager dataScopeManager;
        @Resource
        private DataPermissionContext dataPermissionContext;
        @Resource
        private DataPermissionServiceFacade dataPermissionServiceFacade;
    
        public DataPermissionRequestInterceptor() {
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            try {
                this.doPre(request);
            } catch (Throwable var5) {
                logger.error(var5.getMessage(), var5);
            }
            return true;
        }
    
        private void doPre(HttpServletRequest request) {
            //获取数据范围
            loadDataPermissionContext(request);
            //判断是否数据过滤
            String[] parameterValues = request.getParameterValues(DATA_SCOPE_KEY);
            if (parameterValues != null) {
                long count = Arrays.stream(parameterValues).filter("true"::equals).count();
                if (count > 0) {
                    dataScopeManager.push(true);
                }else{
                    dataScopeManager.push(false);
                }
            }
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
            if (!request.isAsyncStarted()) {
                this.dataScopeManager.clear();
                this.dataPermissionContext.clear();
            }
        }
    
        /**
         * 初始化数据范围上下文
         */
        private void loadDataPermissionContext(HttpServletRequest request) {
            DataPermissionEntity dataScope = dataPermissionServiceFacade.getDataPermissionEntity();
            if (dataScope != null) {
                this.dataPermissionContext.setDataPermissionEntity(dataScope);
            } else if (!request.isAsyncStarted()) {
                this.dataPermissionContext.clear();
            }
        }
    }
    
    public interface DataPermissionServiceFacade {
          // 获取数据权限范围
          DataPermissionEntity getDataPermissionEntity();
    }
    
    

    获取数据范围的业务代码,看下大概逻辑返回,不需要多关注

    
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.util.Collection;
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
       获取数据范围的业务代码不需要多关注
    */
    
    @Component
    @Slf4j
    public class DataPermissionServiceFacadeImpl implements DataPermissionServiceFacade {
        @Autowired
        private SysAuthObjectDetailClient sysAuthObjectDetailClient;
        @Autowired
        private OrgContext orgContext;
        @Autowired
        private UserContext userContext;
        @Autowired
        private SysOrgClient sysOrgClient;
        @Autowired
        private ProductContext productContext;
        @Autowired
        private CurrentUserClient currentUserClient;
        @Override
        public DataPermissionEntity getDataPermissionEntity() {
            QueryTreeOrgReq queryTreeOrgReq = new QueryTreeOrgReq();
            queryTreeOrgReq.setOrgLevel(OrgLevelEnum.GROUP.getCode());
            return queryDataPermissionEntity(queryTreeOrgReq);
        }
    
        /**
         * 获取数据范围
         */
    
        public DataPermissionEntity queryDataPermissionEntity(QueryTreeOrgReq queryTreeOrgReq) {
            log.info("queryDataPermissionEntity: modelId={}, userId={}, orgId={}",productContext.getModeId(), userContext.getId(), orgContext.getId());
    
            DataPermissionEntity dataPermissionEntity = new DataPermissionEntity();
            if (currentUserClient.isTenantAdmin()) {
                return dataPermissionEntity;
            }
    
            // 没有模块ID不查询数据范围
            if (org.apache.commons.lang3.StringUtils.isEmpty(productContext.getModeId())) {
                return dataPermissionEntity;
            }
            // 查询当前用户当前模块的数据范围
            List<AuthDataModel> authDataModels = sysAuthObjectDetailClient.queryModelDataAuthList(productContext.getModeId());
            log.info("queryDataPermissionEntity:authDataModels={}", authDataModels);
            if (CollectionUtils.isEmpty(authDataModels)) {
                return dataPermissionEntity;
            }
    
            // 授权列表中不存在数据范围权限的,展示全部
            long count = authDataModels.stream().filter(item -> !DataAuthCodeEnum.NO_AUTH.getCode().equals(item.getCode())).count();
            if (count <= 0) {
                long noAuthCount = authDataModels.stream().filter(item -> DataAuthCodeEnum.NO_AUTH.getCode().contains(item.getCode())).count();
                dataPermissionEntity.setSysOrgModelList(sysOrgClient.tree(queryTreeOrgReq));
                if(noAuthCount > 0){
                    dataPermissionEntity.setNotAuth(true);
                }
                return dataPermissionEntity;
            }
    
            // 授权列表中存在数据范围的,以存在的范围为准
            List<String> orgIds = authDataModels.stream().filter(item -> !DataAuthCodeEnum.SELF.getCode().equals(item.getCode()))
                    .map(AuthDataModel::getData).flatMap(Collection::stream).collect(Collectors.toList());
            if (!org.springframework.util.CollectionUtils.isEmpty(orgIds)) {
                for (AuthDataModel item : authDataModels) {
                    boolean isQueryAll = isQueryGroupData(item);
                    if(isQueryAll){
                        dataPermissionEntity.setQueryAll(true);
                        return dataPermissionEntity;
                    }
                }
                dataPermissionEntity.setOrgIds(orgIds);
            }
    
            // 授权列表中存在本人的,查询本人创建的组织
            List<String> userIds = authDataModels.stream().filter(item -> DataAuthCodeEnum.SELF.getCode().equals(item.getCode()))
                    .map(AuthDataModel::getData).flatMap(Collection::stream).collect(Collectors.toList());
            if (!org.springframework.util.CollectionUtils.isEmpty(userIds)) {
                dataPermissionEntity.setUserIds(userIds);
            }
            return dataPermissionEntity;
        }
    
        /**
         * 是否查询全公司数据
         * @param item
         * @return
         */
    
        private boolean isQueryGroupData(AuthDataModel item) {
            String authCode = item.getCode();
            if (DataAuthCodeEnum.ORGANIZATION_RANGE.getCode().equals(authCode)
                    || DataAuthCodeEnum.THE_ORGANIZATION_CHILDREN.getCode().equals(authCode)) {
                List<String> data = item.getData();
                if(CollectionUtils.isNotEmpty(data)){
                    return data.contains("1");
                }
            }
            return false;
        }
    
    }
    
  • webconfig配置类

    配置web端请求拦截器,拦截所有的web请求,获取数据范围信息存,

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class AuthDataWebMvcConfig implements  WebMvcConfigurer {
    
        @Bean
        public DataPermissionContext dataPermissionContext() {
            return DefaultDataPermissionContext.newDataUserContext();
        }
    
        @Bean
        public DataPermissionRequestInterceptor authDataModelsInterceptor() {
            return new DataPermissionRequestInterceptor();
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
           //拦截前端所有请求 , order(2)是因为原来已经存在WebMvcConfig的配置类,此处需要优先于平台的执行
            registry.addInterceptor(authDataModelsInterceptor()).addPathPatterns("/**").order(2)
                ;
        }
    
    
    
    
        /**
         * 数据范围上下文默认实现
         */
        private static class DefaultDataPermissionContext implements DataPermissionContext {
            //数据范围信息
            private static ThreadLocal<DataPermissionEntity> authCurrentUserOrgThreadLocal = new ThreadLocal<>();
    
    
            public static DefaultDataPermissionContext newDataUserContext() {
                return new DefaultDataPermissionContext();
            }
            private DefaultDataPermissionContext(){
    
            }
    
    
            @Override
            public DataPermissionEntity getDataPermissionEntity() {
                return authCurrentUserOrgThreadLocal.get();
            }
            @Override
            public void setDataPermissionEntity(DataPermissionEntity dataPermissionEntity) {
                authCurrentUserOrgThreadLocal.set(dataPermissionEntity);
            }
            @Override
            public void clear() {
                authCurrentUserOrgThreadLocal.remove();
            }
        }
    }
    
    
  • 数据范围上下文对象

    /**
     * 收据范围上下文
     */
    public interface DataPermissionContext {
        void setDataPermissionEntity(DataPermissionEntity dataPermissionEntity);
        
        DataPermissionEntity getDataPermissionEntity();
        
        void clear();
    }
    
    
  • 数据范围授权实体

    import lombok.Data;
    import java.util.List;
    
    /**
     * 收据范围授权信息
     */
    @Data
    public class DataPermissionEntity {
        private List<SysOrgModel> sysOrgModelList;
    
        /**
         * 无权限
         */
        private boolean notAuth;
        /**
         * 查询全部
         * 公司及以下||自定义范围是公司的
         */
        private boolean queryAll;
    
        /**
         * 组织ID
         */
        private List<String> orgIds;
    
        /**
         * 用户ID
         */
        private List<String> userIds;
    }
    
    
  • MybatisPlusConfig配置类

    配置Mybatis Plus拦截器,数据权限handler作为参数传给拦截器构造方法。

    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.*;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.core.annotation.Order;
    
    import javax.annotation.Resource;
    
    @Configuration
    public class MybatisPlusConfig  {
    
        @Resource
        private DataPermissionSqlInterceptor dataPermissionSqlInterceptor;
        
        @Bean
        @Order(1)
        @Primary
        public MybatisPlusInterceptor mybatisPlusInterceptor(DefaultUpdateInterceptor defaultUpdateInterceptor, NepochTenantHandler nepochTenantHandler) {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    
            interceptor.addInnerInterceptor(dataPermissionSqlInterceptor);
    
            //分页插件,拦截器会导致原始分页肯可鞥失效,需要在处理下
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
            interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
            return interceptor;
        }
        
        /**
         * 分页插件
         * @return
         */
        @Bean
        public PaginationInnerInterceptor paginationInterceptor() {
            return new PaginationInnerInterceptor();
        }
    }
    
  • mybatis_plus拦截处理拼接sql
    核心的方法:
    * select语句进processSelect()
    * 拼接构造语句builderExpression()

    
    import cn.hutool.core.util.BooleanUtil;
    import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
    import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
    import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
    import com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor;
    import lombok.extern.slf4j.Slf4j;
    import net.sf.jsqlparser.expression.Expression;
    import net.sf.jsqlparser.expression.Parenthesis;
    import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
    import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
    import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
    import net.sf.jsqlparser.expression.operators.relational.InExpression;
    import net.sf.jsqlparser.schema.Column;
    import net.sf.jsqlparser.schema.Table;
    import net.sf.jsqlparser.statement.select.Select;
    import net.sf.jsqlparser.statement.select.WithItem;
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.executor.statement.StatementHandler;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.mapping.SqlCommandType;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.sql.Connection;
    import java.sql.SQLException;
    import java.util.Arrays;
    import java.util.List;
    import java.util.Objects;
    import java.util.stream.Collectors;
    
    
    @Slf4j
    @Component
    public class DataPermissionSqlInterceptor  extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
    
        /**
         * *用在数据范围筛选的用户id字段的默认字段名称
         */
        public static final String DEFAULT_USER_ID_FIELD_NAME = "create_user_id";
        /**
         * 用在数据范围筛选的组织1d字段的默认字段名称
         */
        public static final String DEFAULT_ORG_ID_FIELD_NAME = "org_id";
        @Resource
        private DataScopeManager dataScopeManager;
        @Resource
        private DataPermissionContext dataScopeContext;
    
        @Value("${datatable.tableList}")
        private String[] tableList;
    
    
        @Override
        public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
            //没有打开数据范围开关
            Boolean dataScope = dataScopeManager.peek();
            if(!BooleanUtil.isTrue(dataScope)){
                return;
            }
            //没有配置数据范围||数据范围设置的是分公司及以下(全部)
            DataPermissionEntity dataPermission = dataScopeContext.getDataPermissionEntity();
            if (dataPermission == null || dataPermission.isQueryAll()) {
                return;
            }
            //mybatisPlus的忽略配置
            if (!InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
                //执行sql
                PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
                mpBs.sql(this.parserSingle(mpBs.sql(), (Object) null));
            }
        }
    
        @Override
        public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
            PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
            MappedStatement ms = mpSh.mappedStatement();
            SqlCommandType sct = ms.getSqlCommandType();
            if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
                //增删改查暂不处理
                return;
            }
        }
    
        @Override
        protected void processSelect(Select select, int index, String sql, Object obj) {
            String whereSegment = (String) obj;
            this.processSelectBody(select.getSelectBody(), whereSegment);
            List<WithItem> withItemsList = select.getWithItemsList();
            if (!CollectionUtils.isEmpty(withItemsList)) {
                withItemsList.forEach((withItem) -> {
                    this.processSelectBody(withItem, whereSegment);
                });
            }
        }
        /**
           此处拼接条件
        */
        @Override
        protected Expression builderExpression(Expression currentExpression, List<Table> tables, final String whereSegment) {
            if (CollectionUtils.isEmpty(tables)) {
                return currentExpression;
            } else {
                Expression dataScopeTableExpression = this.buildTableExpression(tables.get(0), currentExpression, whereSegment);
                if (dataScopeTableExpression == null) {
                    return currentExpression;
                } else {
                    if (currentExpression == null) {
                        return dataScopeTableExpression;
                    } else {
                        return currentExpression instanceof OrExpression ? new AndExpression(new Parenthesis(currentExpression), (Expression)dataScopeTableExpression) : new AndExpression(currentExpression, (Expression)dataScopeTableExpression);
                    }
                }
            }
        }
    
    
        @Override
        public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
            return builderExpression(table);
        }
    
        /**
         * 处理条件
         */
        public Expression builderExpression(Table table) {
            // 判断该表是否在白名单中 ,不在则是业务表 ,需要拼orgId ,在则是系统表,不需要拼orgId
            if (Arrays.asList(tableList).contains(table.getName())) {
                return null;
            }
          
            //获取之前在ThreadLoca存的数据范围的数据
            DataPermissionEntity dataScope = dataScopeContext.getDataPermissionEntity();
            if (dataScope == null || (
                    CollectionUtils.isEmpty(dataScope.getUserIds())
                            && CollectionUtils.isEmpty(dataScope.getOrgIds())
            )) {
                if (dataScope != null && BooleanUtil.isTrue(dataScope.isNotAuth())) {
                    EqualsTo equalsTo = new EqualsTo();
                    equalsTo.setLeftExpression(new Column("'auth'"));
                    equalsTo.setRightExpression(new Column("'noAuth'"));
                    return equalsTo;
                }
                return null;
            }
    
            InExpression inExpression = new InExpression();
            List<String> orgIds = dataScope.getOrgIds();
            if (CollectionUtils.isNotEmpty(orgIds)) {
                //orgid in 
                inExpression.setLeftExpression(this.getAliasColumn(table, DEFAULT_ORG_ID_FIELD_NAME));
                //('2','212')
                inExpression.setRightExpression(this.getAliasRightColumn(orgIds));
            }
            List<String> userIds = dataScope.getUserIds();
            if (CollectionUtils.isNotEmpty(userIds)) {
                inExpression.setLeftExpression(this.getAliasColumn(table, DEFAULT_USER_ID_FIELD_NAME));
                inExpression.setRightExpression(this.getAliasRightColumn(userIds));
            }
            //inExpression实际就是orgid in ('2','212')
            return inExpression;
        }
       /**
         处理条件后面拼接的值,orgid in ('1','2')
         */
        protected Column getAliasRightColumn(List<String> list) {
            return new Column("(" + String.join(",", list) + ")");
        }
        
          /**
         * 表别名设置
         * org_id 或 tableAlias.org_id
         * @param table 表对象
         * @return 字段
         */
    
        protected Column getAliasColumn(Table table, String columnName) {
            StringBuilder column = new StringBuilder();
            if (table.getAlias() != null) {
                column.append(table.getAlias().getName()).append(".");
            }
    
            column.append(columnName);
            return new Column(column.toString());
        }
    }
    
  • 最终执行结果 :orgid in()

    在这里插入图片描述

到这基本功能就算实现了 ,但是在实际测试中不同的接口有不同的要求 ,有些需要mybatis_plus拦截处理数据范围,有些不需要拦截,继续改造

改造过程

​ 基于上面说到的有些需要拦截,有些不需要 ,不能一概而论;继续改造;

​ 比如材料类别 ,供应商类别等都是系统公共数据 ,所有数据对全组织都可用 ,此时就不需要拦截拼接数据范围信息;

此处采用注解结合aop方式定义此接口是否需要mybatis_plus拦截处理数据范围;

  • 定义注解

    import java.lang.annotation.*;
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface DataScope {
    
        boolean value() default true;
    
    }
    
    
  • 定义aop Aspect处理类

    package com.glodon.nepoch.zcgl.dataScope.aspect;
    
    import com.glodon.nepoch.zcgl.dataScope.annotation.DataScope;
    import com.glodon.nepoch.zcgl.dataScope.context.DataScopeManager;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.lang.reflect.Method;
    
    /**
     * 数据范围过滤
     */
    @Aspect
    @Component
    public class DataScopeAnnotationAspect {
        @Resource
        private DataScopeManager dataScopeManager;
    
        /**
         * 定义切点,切点为对应controller
         */
        @Pointcut("@annotation(com.glodon.nepoch.zcgl.dataScope.annotation.DataScope)")
        public void aopPointCut(){
    
        }
    
        @Around("aopPointCut()")
        public Object aroundMethod(ProceedingJoinPoint point) throws Throwable{
            DataScope dataScope = getAnnotation(point);
            try {
                beforeProcess(dataScope);
                return methodProcess(point);
            } finally {
                afterProcess(dataScope);
            }
        }
    
        private static Object methodProcess(ProceedingJoinPoint point) throws Throwable {
            Object[] args = point.getArgs();
            //获取参数
            if(args != null){
                return point.proceed(args);
            }else{
                return point.proceed();
            }
        }
    
        private void afterProcess(DataScope dataScope) {
            if (dataScope != null) {
                dataScopeManager.pop();
            }
        }
    
        private void beforeProcess(DataScope dataScope) {
            if (dataScope != null) {
                //获取注解上的值
                boolean open = dataScope.value();
                dataScopeManager.push(open);
            }
        }
    
        //获取注解
        public DataScope getAnnotation(ProceedingJoinPoint point) {
            Signature signature = point.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();
            if (method != null){
                return method.getAnnotation(DataScope.class);
            }
            return null;
        }
    }
    
  • 定义DataScopeManager存储注解信息

    
    import org.springframework.stereotype.Component;
    import java.util.Stack;
    
    @Component
    public class DataScopeManager {
        private final static ThreadLocal<Stack<Boolean>> DATA_SCOPE_STACK_THREAD_LOCAL = new ThreadLocal<>();
    
        public void push(Boolean open) {
            Stack<Boolean> stack = DATA_SCOPE_STACK_THREAD_LOCAL.get();
            if(stack == null){
                DATA_SCOPE_STACK_THREAD_LOCAL.set(new Stack<>());
            }
            DATA_SCOPE_STACK_THREAD_LOCAL.get().push(open);
        }
        public Boolean next() {
            pop();
            return  peek();
        }
    
        public Boolean peek() {
            Stack<Boolean> stack = DATA_SCOPE_STACK_THREAD_LOCAL.get();
            if (stack == null || stack.isEmpty()) {
                return null;
            }
            return stack.peek();
        }
    
    
        public void pop() {
            Stack<Boolean> stack = DATA_SCOPE_STACK_THREAD_LOCAL.get();
            if (stack == null || stack.isEmpty()) {
                return;
            }
            stack.pop();
        }
    
        public void clear(){
            DATA_SCOPE_STACK_THREAD_LOCAL.remove();
        }
    }
    
    
  • Controller方法上增加注解

    此处是在代码层增加注解,还可在接口中在在增加路径参数(如下)

    @ApiOperation(value = "导出详情Excel", notes = "列表的导出-预损单详情")
        @PostMapping("/export/details")
        @DataScope
        public void exportDetailsExcel(@RequestBody( required = false) PreLossQueryDTO query, HttpServletResponse response) {
            preLossService.exportDetailsExcel(response, query);
        }
    
  • http接口中增加路径参数dataScope=true

    • http://127.0.0.1/md/listByPage?dataScope=true
      
    • http拦截器中增加路径参数判断

      private void doPre(HttpServletRequest request) {
              //获取数据范围
              loadDataPermissionContext(request);
              //判断是否接口中有数据过滤
              String[] parameterValues = request.getParameterValues(DATA_SCOPE_KEY);
              if (parameterValues != null) {
                  long count = Arrays.stream(parameterValues).filter("true"::equals).count();
                  if (count > 0) {
                      dataScopeManager.push(true);
                  }else{
                      dataScopeManager.push(false);
                  }
              }
          }
      
  • mybatis_plus拦截处理数据范围代码beforeQuery方法中增加

     //没有打开数据范围开关
            Boolean dataScope = dataScopeManager.peek();
            if(!BooleanUtil.isTrue(dataScope)){
                return;
            }
    

总结

​ 到此基本已经实现 ,满足使用场景 ,此次只是针对查询进行改造,所作的所有最终就是为了给查询条件后面拼接上其他条件,其他的增删改也原理相同;次此采用了注解、aop面向切面、http拦截器、ThreadLocal线程局部变量、mybatis_plus拦截器等实现了此功能,此处没有对mybatis_plus拦截器的源码做过多的说明 ,过程中用到的这些技术如若不熟悉,对此感兴趣的可以看其他博主的说明;此案例还是很具有代表性的功能 ,租户隔离 本此的权限后补等等;


http://www.kler.cn/a/287429.html

相关文章:

  • 使用 OpenAI 进行数据探索性分析(EDA)
  • SpringBoot Data Redis连接Redis-Cluster集群
  • React Native 全栈开发实战班 - 数据管理与状态之Zustand应用
  • 在 CentOS 7 上安装 MinIO 的步骤
  • [Qt platform plugin问题] Could not load the Qt platform plugin “xcb“
  • LeetCode 2816.翻倍以链表形式表示的数字
  • 秋招/春招投递公司记录表格
  • 公司来了个大佬,把FullGC 40次/天优化为10天1次,太秀了~!
  • 每天学习一个基础算法之二分查找
  • Python 生成随机的国内 ip
  • 视觉SLAMch4——李群和李代数
  • 单机无法拨号问题分析
  • UI自动化测试的边界怎么定义?
  • python中的值传递和引用传递
  • 城投公司相关指标数据(2023.8)
  • springboot+vue 进销存管理系统
  • 一起学习LeetCode热题100道(61/100)
  • 计算图像分割mask的灰度级个数、以及删除空的分割数据
  • HTML静态网页成品作业(HTML+CSS)——动漫猫和老鼠网页(1个页面)
  • 快速安全部署 Tomcat
  • 全志Linux磁盘操作基础命令
  • 程序化交易在中国的规模
  • 云计算实训39——Harbor仓库的使用、Docker-compose的编排、YAML文件
  • 什么场景可以使用函数式接口
  • 【数据结构】线性表的链式表示(单链表)
  • 《C++20 特性综述》