Mybatis实现数据权限控制

## Mybatis实现方式

1.实现思想

利用Mybatis中提供的方法,可以实现SQL的提前拦截然后重新进行sql拼装的方式来让查询sql做到数据隔离的效果

2.具体实现过程

注:实现过程仅供参考,具体定义什么字段,如何实现根据实际业务进行定义

  1.  第一步是创建使用的注解,注解主要包含了作用表的别名 tableAlias(),以及默认所需维护的仓库库位id字段warehouseField()
    /**
     * 数据权限控制注解
     * 默认加上该注解会进行数据权限控制,相关业务不需要数据权限控制的话无需加上该注解
     * 该注解使用在方法上
     */
    @Target({ElementType.METHOD,ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface FactoryDataScope {
        /**
         * 作用表的别名
         */
        String tableAlias() default "";
        /**
         *仓库库位表字段名
         */
        String warehouseField() default "warehouse_field";
    }
  2.  创建一个获取权限集合的工具类,具体如何实现根据自己的项目进行开发,最终实现的效果是获取如某个用户的权限集合
    /**
     * 获取用户信息
     * 用于获取用户的相关角色信息、权限信息
     */
    @Slf4j
    @Service
    public class WarehousePermissionUtils {
        @Autowired
        private AuthFeignClient authFeignClient;
        @Autowired
        private StoreFeignClients storeFeignClients;
    
        //获取用户权限集合
        public Map<String, List<Integer>> getPermission(){
            Map<String, List<Integer>> permissionMap = new HashMap<>();
            //存放最终的所以仓库id集合
            List<Integer> warehouseModels = new ArrayList<>();
            //获取当前用户id
            Integer userId =  BaseContextHandler.getUserID();
            //根据当前用户id获取对应的数据角色集合
            Result<List<Integer>> rolesByUserIds = authFeignClient.getRolesByUserIds(userId, 2);
            if(ObjectUtils.isNotEmpty(rolesByUserIds)){
                rolesByUserIds.getData().stream().forEach(rolesByUserId->{
                    //获取当前用户所有的仓库权限
                    Result<List<AuthRoleWarehouse>> authRoleWarehouse = authFeignClient.getRoleWarehouse(rolesByUserId);
                    List<Integer> warehouseIds = authRoleWarehouse.getData().stream().map(warehouse -> {
                        return warehouse.getWarehouseId();
                    }).collect(Collectors.toList());
                    warehouseModels.addAll(warehouseIds);
    //                warehouseIds.stream().forEach(warehouseId->{
    //                    //获取当前仓库节点的所有子节点id
    //                    Result<List<Long>> WarehouseModelDtos = storeFeignClients.getByWarehouseModelId(Long.parseLong(String.valueOf(warehouseId)));
    //                    List<Integer> warehouseModelId = WarehouseModelDtos.getData().stream().map(warehouseModelDto -> {
    //                        return Integer.parseInt(String.valueOf(warehouseModelDto));
    //                    }).collect(Collectors.toList());
    //                    warehouseModels.addAll(warehouseModelId);
    //                });
                });
                //有些父节点重复了,进行去重
                List<Integer> result = warehouseModels.stream().distinct().collect(Collectors.toList());
                permissionMap.put("warehouseModel",result);
            }
            return permissionMap;
        }
    
    }
  3. 编写一个数据过滤的配置类,主要实现方式是通过本地线程的相关特性来实现原有sql的替换,同时可以在获取到注解的同时做一些权限处理,如超级管理员的sql处理等
    import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
    import com.sw.mes.core.annotation.WarehouseDataScope;
    import com.sw.mes.core.context.BaseContextHandler;
    import com.sw.mes.core.utils.WarehousePermissionUtils;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;
    import net.sf.jsqlparser.expression.Expression;
    import net.sf.jsqlparser.expression.HexValue;
    import net.sf.jsqlparser.expression.LongValue;
    import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
    import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
    import net.sf.jsqlparser.expression.operators.relational.InExpression;
    import net.sf.jsqlparser.expression.operators.relational.ItemsList;
    import net.sf.jsqlparser.schema.Column;
    import org.apache.commons.collections.CollectionUtils;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    @Aspect
    @Slf4j
    @Component
    public class WarehouseFilterConfig implements DataPermissionHandler {
        /**
         * 通过ThreadLocal记录权限相关的属性值
         */
        ThreadLocal<DataScopeParam> threadLocal = new ThreadLocal<>();
        @Autowired
        private WarehousePermissionUtils userUtils;
    
        /**
         * 清空当前线程上次保存的权限信息
         */
        @After("dataScopePointCut()")
        public void clearThreadLocal(){
            threadLocal.remove();
        }
    
        /**
         * 注解对象
         * 该成员变量在并发情况下导致多个线程公用了一套 @DataScope 配置的参数,导致sql拼接失败,必须改为局部变量
         * 修改于:2022.04.28
         */
    //    private DataScope controllerDataScope;
    
        /**
         * 配置织入点
         */
        @Pointcut("@annotation(com.sw.mes.core.annotation.WarehouseDataScope)")
        public void dataScopePointCut() {
        }
    
        @Before("dataScopePointCut()")
        public void doBefore(JoinPoint point) {
            // 获得注解
            WarehouseDataScope controllerDataScope = getAnnotationLog(point);
            if (controllerDataScope != null) {
                // 获取当前的用户及相关属性,需提前获取和保存数据权限对应的部门ID集合
                Map<String, List<Integer>> permission = userUtils.getPermission();
                DataScopeParam dataScopeParam = new DataScopeParam(controllerDataScope.tableAlias(),
                        controllerDataScope.warehouseField(),
                        BaseContextHandler.getUsername().equals("admin")?false:false,
                        permission.get("warehouseModel"));
                threadLocal.set(dataScopeParam);
            }
        }
    
        /**
         * 是否存在注解,如果存在就获取
         */
        private WarehouseDataScope getAnnotationLog(JoinPoint joinPoint) {
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();
            if (method != null) {
                return method.getAnnotation(WarehouseDataScope.class);
            }
            return null;
        }
    
        /**
         * @param where             原SQL Where 条件表达式
         * @param mappedStatementId Mapper接口方法ID
         * @return
         */
        @SneakyThrows
        @Override
        public Expression getSqlSegment(Expression where, String mappedStatementId) {
            DataScopeParam dataScopeParam = threadLocal.get();
            if(dataScopeParam == null || dataScopeParam.isAdmin()){
                return where;
            }
    
            if (where == null) {
                where = new HexValue(" 1 = 1 ");
            }
    
            String deptSql = "".equals(dataScopeParam.tableAlias) ? dataScopeParam.warehouseField : dataScopeParam.tableAlias + "." + dataScopeParam.warehouseField;
    
            // 把集合转变为JSQLParser需要的元素列表
            ItemsList itemsList;
            if(CollectionUtils.isEmpty(dataScopeParam.secretary)){
                //如果权限为空,则只能看自己部门的
                itemsList = null;
            }else {
                //查看权限内的数据
                itemsList = new ExpressionList(dataScopeParam.secretary.stream().map(LongValue::new).collect(Collectors.toList()));
            }
            InExpression inExpression = new InExpression(new Column(deptSql), itemsList);
            return new AndExpression(where, inExpression);
    
        }
    
        /**
         * ThreadLocal存储对象
         */
        @Data
        @AllArgsConstructor
        static class DataScopeParam{
            /**
             * 作用表别名
             */
            private String tableAlias;
    
            /**
             * 仓库作用字段名
             */
            private String warehouseField;
    
            /**
             * 是否是管理员
             */
            private boolean isAdmin;
    
            /**
             * 数据权限范围
             */
            private List<Integer> secretary;
        }
    }
  4. 把自定义好的类注入到MyBatisCofig配置类中,这样才能保障每次需要执行SQL时都会走自定义的类
    @Configuration
    @MapperScan("com.sw.mes.store.mapper")
    public class MybatisConfig {
    
        @Autowired
        @Lazy
        private WarehouseFilterConfig warehouseFilterConfig;
        /**
         * 数据库驱动类
         */
        @Value("${jdbc.driverClassName}")
        private String driverClassName;
    
        /**
         * 数据库路径测试环境
         */
        @Value("${jdbc.url}")
        private String url;
        /**
         * 数据库名称
         */
        @Value("${jdbc.username}")
        private String username;
    
        /**
         * 数据库密码
         */
        @Value("${jdbc.password}")
        private String password;
    
        /**
         * 默认提交
         */
        private static final boolean defaultAutoCommit = true;
    
        /**
         * 连接池的最大数据库连接数。设为0表示无限制。
         */
        private static final int maxActive = 150;
    
        /**
         * 最大空闲数,数据库连接的最大空闲时间。超过空闲时间,数据库连接将被标记为不可用,然后被释放。设为0表示无限制。
         */
        private static final int maxIdle = 100;
    
        /**
         * 最小空闲数,
         */
        private static final int minIdle = 5;
    
        /**
         * 最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
         */
        private static final int maxWait = 60000;
    
        @Bean
        public DataSource getDataSource() {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName(driverClassName);
            dataSource.setUrl(url);
            dataSource.setUsername(username);
            dataSource.setPassword(password);
            dataSource.setDefaultAutoCommit(defaultAutoCommit);
            dataSource.setMaxActive(maxActive);
            dataSource.setMinIdle(minIdle);
            dataSource.setMaxWait(maxWait);
            dataSource.setTestWhileIdle(true);
            dataSource.setValidationQuery("select 1");
            dataSource.setTestOnBorrow(true);
            dataSource.setTestOnReturn(false);
            dataSource.setRemoveAbandoned(true);
            dataSource.setRemoveAbandonedTimeout(120);
            dataSource.setTimeBetweenEvictionRunsMillis(30000);
            dataSource.setMinEvictableIdleTimeMillis(1800000);
            return dataSource;
        }
    
        @Bean
        public MybatisSqlSessionFactoryBean getSqlSessionFactory(DataSource dataSource) throws Exception {
            MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
            mybatisSqlSessionFactoryBean.setDataSource(dataSource);
            mybatisSqlSessionFactoryBean.setTypeAliasesPackage("com.sw.mes.store.entity");
            PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            mybatisSqlSessionFactoryBean.setConfigLocation(resolver.getResource("classpath:config/mybatis-config.xml"));
            mybatisSqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            // 添加数据权限插件
            DataPermissionInterceptor warehousePermission = new DataPermissionInterceptor();
            // 添加自定义的数据权限处理器
            warehousePermission.setDataPermissionHandler(warehouseFilterConfig);
            interceptor.addInnerInterceptor(warehousePermission);
            //设置分页插件
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
            Interceptor[] plugins = {interceptor};
            mybatisSqlSessionFactoryBean.setPlugins(plugins);
            return mybatisSqlSessionFactoryBean;
        }
    
    
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    
    
    }
    

  5. 注解使用说明:
    该注解使用的对象实在具体的方法上,如查询分页列表接口可以直接在上面加上注解,注解中有两个参数,一个是作用表的别名,一个是作用字段,目前设计的是字段默认是warehouse_id,如果有变化需要手动在注解后把第二个参数填入@WarehouseDataScope:仓库数据权限注解,该方法需要按照仓库模式数据权限过滤则加上该注解,否则反之
    -- tableAlias: 作用表别名参数 必填
    -- warehouseField: 作用字段参数 选填,不填则填入默认值

总结:思路永远都是引路者,不是实现者,具体实现还是要根据自己的实际业务做。嗯~在码农的路上保持分享!!!

JPA的数据权限也做了,但是做的不是很完美,后面完善之后在分享吧!

  • 29
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值