1024创新实验室-公告

助力抖音1000个粉丝,开播写代码🎉🎉

打开【抖音APP】-点击【左上角侧边栏】-【点击扫一扫】-【进行关注】🎉🎉


和1024创新实验室一起,热爱代码,热爱生活,永远年轻,永远前行🎉🎉


Skip to content

数据范围

一、背景与问题

1024实验室下有三个销售部,分别是销售一部、销售二部、销售三部,卓主任要求销售各部门只能看到各部门自己的订单数据,以防数据外泄恶意竞争。

二 、具体使用

  1. DataScopeTypeEnum新增ORDER枚举项
  2. 创建销售角色,给销售部的人分配此角色
  3. 设置此角色的订单业务模块的数据范围为本人
  4. 对应订单查询方法(Dao接口方法)添加数据范围注解@DataScope注解
java
    /**
     * 订单分页查询
     *
     * @param page
     * @param queryForm
     * @return
     */
    @DataScope(dataScopeType = DataScopeTypeEnum.ORDER, whereInType = DataScopeWhereInTypeEnum.EMPLOYEE, joinSql = "create_user_id in (#employeeIds)")
    List<OrderVO> queryByPage(Page page, @Param("queryForm") OrderQueryForm queryForm);

三、@DataScope注解说明

3.1、@DataScope注解

参数类型说明
dataScopeTypeDataScopeTypeEnum定义对应数据范围的业务模块
whereInTypeDataScopeWhereInTypeEnum定义数据范围Sql拼接的模式是已部门、员工还是自定义的方式判断数据范围
joinSqlImplClazzDataScopePowerStrategy这个是扩展功能,当whereInType的值为CUSTOM_STRATEGY必填,用于固有功能无法满足需求的情况下,通过实现joinSqlImplClazz的方式来自定义数据范围
paramNameString这个同样是扩展功能,用于自定义数据范围策略的实现方法中获取接口参数的内容
whereIndexint默认值0,定义拼接的sql从第几个Where开始
joinSqlString拼接的sql语句,非自定义策略此参数必填,目前扩展的参数变量有#departmentIds、#employeeIds

3.2、joinSql参数说明

参数说明
#departmentIdswhereInTypeDEPARTMENT时,此参数会自动根据对应用户在此模块的数据范围设置此变量的部门id集合,会自动拼接()
#employeeIdswhereInTypeEMPLOYEE时,此参数会自动根据对应用户在此模块的数据范围设置此变量的员工id集合,会自动拼接()

3.3、joinSqlImplClazz参数说明

java
    /**
     * 数据范围策略 ,使用DataScopeWhereInTypeEnum.CUSTOM_STRATEGY类型,DataScope注解的joinSql属性无用
     *
     * @Author 1024创新实验室: 罗伊
     * @Date 2020/11/28  20:59:17
     * @Wechat zhuoda1024
     * @Email lab1024@163.com
     * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
     */
    public abstract class AbstractDataScopeStrategy {
    
        /**
         * 获取joinsql 字符串
         */
        public abstract String getCondition(DataScopeViewTypeEnum viewTypeEnum, Map<String, Object> paramMap, DataScopeSqlConfig sqlConfigDTO);
    }

当通过框架内置的参数变量#departmentIds、#employeeIds无法控制数据范围需要自定义人员或部门的获取方式时,可以使用自定义策略。 自定义策略需要设置whereInType的值为CUSTOM_STRATEGY,切实现AbstractDataScopeStrategy抽象类。

AbstractDataScopeStrategy参数说明

参数说明
viewTypeEnum当前业务模块操作人所处的查看类型是哪种(本人、本部门、本部门及下属子部门。全部)
paramMap当前Dao层方法的参数集合
sqlConfigDTO当前Dao层方法@DataScope的注解配置

AbstractDataScopeStrategy返回说明:

返回值为字符串,具体内容就是需要拼接的sql语句,如"(w.create_user_id in (1,2,3,4) or o.create_user_id in (11,12,13,14))"

四、实现原理

实现原理:

在日常项目开发过程中,通常是在不考虑权限、数据范围的情况下进行开发的,那么怎么实现后期数据范围的动态配置是首选要考虑解决的问题。 通过查看Mybatis项目文档,发现Mybatis具有插件功能,允许你在映射语句执行过程中的某一点进行拦截调用,那么我们可以自定义一个插件 在项目执行查询方法时通过动态拼装一些SQL实现数据访问的动态配置。

注:Mybatis项目文档请查看:https://mybatis.org/mybatis-3/zh_CN/configuration.html#plugins

实现步骤:

1.自定义数据访问注解@DataScope,用于获取各业务模块数据范围的配置方式。同时为方便获取配置信息我们在项目启动时自动将所有添加过此注解的Mybatis接口方法加入到了系统缓存中。 参考:DataScopeSqlConfigService#initDataScopeMethodMap

java
    /**
     * 刷新 所有添加数据范围注解的接口方法配置<class.method,DataScopeSqlConfigDTO></>
     */
    private Map<String, DataScopeSqlConfig> refreshDataScopeMethodMap() {
        Reflections reflections = new Reflections(new ConfigurationBuilder().setUrls(ClasspathHelper.forPackage(AdminApplication.COMPONENT_SCAN)).setScanners(new MethodAnnotationsScanner()));
        Set<Method> methods = reflections.getMethodsAnnotatedWith(DataScope.class);
        for (Method method : methods) {
            DataScope dataScopeAnnotation = method.getAnnotation(DataScope.class);
            if (dataScopeAnnotation != null) {
                DataScopeSqlConfig configDTO = new DataScopeSqlConfig();
                configDTO.setDataScopeType(dataScopeAnnotation.dataScopeType());
                configDTO.setJoinSql(dataScopeAnnotation.joinSql());
                configDTO.setWhereIndex(dataScopeAnnotation.whereIndex());
                configDTO.setDataScopeWhereInType(dataScopeAnnotation.whereInType());
                configDTO.setParamName(dataScopeAnnotation.paramName());
                configDTO.setJoinSqlImplClazz(dataScopeAnnotation.joinSqlImplClazz());
                dataScopeMethodMap.put(method.getDeclaringClass().getSimpleName() + "." + method.getName(), configDTO);
            }
        }
        return dataScopeMethodMap;
    }

    /**
     * 根据调用的方法获取,此方法的配置信息
     *
     */
    public DataScopeSqlConfig getSqlConfig(String method) {
        return this.dataScopeMethodMap.get(method);
    }

    /**
     * 组装需要拼接的sql
     */
    public String getJoinSql(Map<String, Object> paramMap, DataScopeSqlConfig sqlConfigDTO) {
        DataScopeTypeEnum dataScopeTypeEnum = sqlConfigDTO.getDataScopeType();
        String joinSql = sqlConfigDTO.getJoinSql();
      	// 获取当前请求的用户信息
        Long employeeId = SmartRequestUtil.getRequestUserId();
        // 由于数据范围依托于员工信息,如果员工不存在时,就没必要拼装sql了
        if (employeeId == null) {
            return "";
        }
        // 使用的是自定义策略的情况
        if (DataScopeWhereInTypeEnum.CUSTOM_STRATEGY == sqlConfigDTO.getDataScopeWhereInType()) {
            Class strategyClass = sqlConfigDTO.getJoinSqlImplClazz();
            if (strategyClass == null) {
                log.warn("data scope custom strategy class is null");
                return "";
            }
            // 获取当前自定义策略的bean对象
            AbstractDataScopeStrategy powerStrategy = (AbstractDataScopeStrategy) applicationContext.getBean(sqlConfigDTO.getJoinSqlImplClazz());
            if (powerStrategy == null) {
                log.warn("data scope custom strategy class:{} ,bean is null", sqlConfigDTO.getJoinSqlImplClazz());
                return "";
            }
          	// 获取当前员工的可见范围
            DataScopeViewTypeEnum viewTypeEnum = dataScopeViewService.getEmployeeDataScopeViewType(dataScopeTypeEnum, employeeId);
          	// 执行自定义策略,返回需要拼装的sql
            return powerStrategy.getCondition(viewTypeEnum,paramMap, sqlConfigDTO);
        }
        // 以员工为维度进行可见范围判断的时候
        if (DataScopeWhereInTypeEnum.EMPLOYEE == sqlConfigDTO.getDataScopeWhereInType()) {
           	// 通过系统预定义的查询可见范围的方法,获取当前员工可以看到哪些人员的数据
            List<Long> canViewEmployeeIds = dataScopeViewService.getCanViewEmployeeId(dataScopeTypeEnum, employeeId);
            if (CollectionUtils.isEmpty(canViewEmployeeIds)) {
                return "";
            }
            String employeeIds = StringUtils.join(canViewEmployeeIds, ",");
          	// 将查询到的人员id替换掉系统预定义的参数#employeeIds
            String sql = joinSql.replaceAll(EMPLOYEE_PARAM, employeeIds);
            return sql;
        }
       // 以部门为维度进行可见范围判断的时候
        if (DataScopeWhereInTypeEnum.DEPARTMENT == sqlConfigDTO.getDataScopeWhereInType()) {
          	// 通过系统预定义的查询可见范围的方法,获取当前员工可以看到哪些部门的数据
            List<Long> canViewDepartmentIds = dataScopeViewService.getCanViewDepartmentId(dataScopeTypeEnum, employeeId);
            if (CollectionUtils.isEmpty(canViewDepartmentIds)) {
                return "";
            }
            String departmentIds = StringUtils.join(canViewDepartmentIds, ",");
          	// 将查询到的人员id替换掉系统预定义的参数#departmentIds
            String sql = joinSql.replaceAll(DEPARTMENT_PARAM, departmentIds);
            return sql;
        }
        return "";
    }

2.自定义Mybatis插件,用于各业务模块在执行Sql查询时动态拼接Sql。实现数据范围的核心代码在此模块,接下来我们来详细看下此部分代码

java
public Object intercept(Invocation invocation) throws Throwable {
				// 获取当前执行的MappedStatement对象,在MyBatis每一个<select>, <insert>, <update>, <delete>标签都会被解析成一个MappedStatement对象
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
  			// 获取当前调用Dao接口方法的参数信息
        Object parameter = invocation.getArgs()[1];
				// 获取此次执行的Sql信息
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        String originalSql = boundSql.getSql().trim();
  			// 获取此次调用MappedStatement对象的ID,此ID用于获取第一步存入到系统缓存中配置内容,系统的缓存key是【类名.方法名】
        String id = mappedStatement.getId();
        List<String> methodStrList = StrUtil.split(id, ".");
        String path = methodStrList.get(methodStrList.size() - 2) + "." + methodStrList.get(methodStrList.size() - 1);
  			// 通过applicationContext获取系统配置Service
        DataScopeSqlConfigService dataScopeSqlConfigService = this.dataScopeSqlConfigService();
  			// 未获取到 Mybatis直接按照当前XML内的SQL内容进行处理
        if (dataScopeSqlConfigService == null) {
            return invocation.proceed();
        }
  			// 获取系统配置Service中的配置信息
        DataScopeSqlConfig sqlConfigDTO = dataScopeSqlConfigService.getSqlConfig(path);
        if (sqlConfigDTO != null) {
            // 获取当前执行Dao的参数信息
            Map<String, Object> paramMap = this.getParamList(sqlConfigDTO.getParamName(), parameter);
          	// 获取拼装过最新SQL语句的BoundSql
            BoundSql newBoundSql = copyFromBoundSql(mappedStatement, boundSql, this.joinSql(originalSql, paramMap, sqlConfigDTO));
            ParameterMap map = mappedStatement.getParameterMap();
          	// 获取最新MappedStatement
            MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql), map);
            invocation.getArgs()[0] = newMs;
        }
				// Mybatis执行返回结果
        Object obj = invocation.proceed();
        return obj;
    }


    private Map<String, Object> getParamList(String paramName, Object parameter) {
        Map<String, Object> paramMap = Maps.newHashMap();
        if (StringUtils.isEmpty(paramName)) {
            return paramMap;
        }
        if (parameter == null) {
            return paramMap;
        }
        if (parameter instanceof Map) {
            String[] paramNameArray = paramName.split(",");
            Map<?, ?> parameterMap = (Map) parameter;
            for (String param : paramNameArray) {
                if(parameterMap.containsKey(param)){
                    paramMap.put(param, parameterMap.get(param));
                }
            }
        }
        return paramMap;
    }

    private String joinSql(String sql, Map<String, Object> paramMap, DataScopeSqlConfig sqlConfigDTO) {
        if (null == sqlConfigDTO) {
            return sql;
        }
      	// 获取需要拼装的Sql语句
        String appendSql = this.dataScopeSqlConfigService().getJoinSql(paramMap, sqlConfigDTO);
        if (StringUtils.isEmpty(appendSql)) {
            return sql;
        }
        // 获取Sql语句需要拼接在第几个Where语句后面
        Integer appendSqlWhereIndex = sqlConfigDTO.getWhereIndex();
        String where = "where";
        String order = "order by";
        String group = "group by";
        // 获取where关键字的索引位置
        int whereIndex = StringUtils.ordinalIndexOf(sql.toLowerCase(), where, appendSqlWhereIndex + 1);
        int orderIndex = sql.toLowerCase().indexOf(order);
        int groupIndex = sql.toLowerCase().indexOf(group);
      	// 原始sql存在where关键字的时候
        if (whereIndex > -1) {
            String subSql = sql.substring(0, whereIndex + where.length() + 1);
            subSql = subSql + " " + appendSql + " AND " + sql.substring(whereIndex + where.length() + 1);
            return subSql;
        }
				// 原始sql不存在where关键字,但存在group by的时候
        if (groupIndex > -1) {
            String subSql = sql.substring(0, groupIndex);
            subSql = subSql + " where " + appendSql + " " + sql.substring(groupIndex);
            return subSql;
        }
      	// 原始sql不存在where group by关键字,但存在order by的时候
        if (orderIndex > -1) {
            String subSql = sql.substring(0, orderIndex);
            subSql = subSql + " where " + appendSql + " " + sql.substring(orderIndex);
            return subSql;
        }
        sql += " where " + appendSql;
        return sql;
    }
	
		public DataScopeSqlConfigService dataScopeSqlConfigService() {
        return (DataScopeSqlConfigService) applicationContext.getBean("dataScopeSqlConfigService");
    }

3.将自定义的插件加入到数据源中,参考DataSourceConfig#sqlSessionFactory

java
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setDataSource(druidDataSource());
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources("classpath*:/mapper/**/*.xml");
        factoryBean.setMapperLocations(resources);

        // 设置 MyBatis-Plus 分页插件 注意此处myBatisPlugin一定要放在后面,特别注意以防连接器被覆盖
        List<Interceptor> pluginsList = new ArrayList<>();
      	// 分页插件
        pluginsList.add(paginationInterceptor);
        if (dataScopePlugin != null) {
          	// 数据范围插件
            pluginsList.add(dataScopePlugin);
        }
        factoryBean.setPlugins(pluginsList.toArray(new Interceptor[pluginsList.size()]));
        // 添加字段自动填充处理
        factoryBean.setGlobalConfig(new GlobalConfig().setBanner(false).setMetaObjectHandler(new MybatisPlusFillHandler()));

        return factoryBean.getObject();
    }

联系我们

1024创新实验室-主任:卓大,混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室 致力于成为中原领先、国内一流的技术团队, 以AI+数字化为驱动,用技术为产业互联网提供无限可能, 业务如下:
  • 教育(就业创业大数据平台、继续教育平台、在线教育系统、题库、医学考试、专升本等)
  • 供应链(网络货运、大宗贸易进销存ERP、物流TMS、B2B电商、仓储WMS、AI提效等)
  • 中医大健康(诊所数字化管理、AI辅助诊疗、中医适宜技术、在线问诊、空中药房等)
  • AI+软件(软件定制外包、数据大屏、国产化改造、人员外包、技术顾问、技术培训等)
  • 欢迎各类合作哦,一起赚钱~
加微信: 卓大
拉你入群,一起学习
公众号 :六边形工程师
分享:赚钱、代码、生活
请 “1024创新实验室”
烩面里加肉
咖啡配胡辣汤,提神又饱腹
抖音 : 六边形工程师
直播:赚钱、代码、中医