使用MyBatis-Plus实现数据权限功能

发布于:2025-06-12 ⋅ 阅读:(19) ⋅ 点赞:(0)

什么是数据权限

数据权限是指系统根据用户的角色、职位或其他属性,控制用户能够访问的数据范围。与传统的功能权限(菜单、按钮权限)不同,数据权限关注的是数据行级别的访问控制。

常见的数据权限控制方式包括:

  • 部门数据权限:只能访问本部门数据

  • 个人数据权限:只能访问自己的数据

  • 自定义数据范围:通过特定规则限制数据访问

MyBatis-Plus实现数据权限的方案

实现完整例子

下面是一个完整的基于MyBatis-Plus和Spring Security的数据权限实现示例:

 1. 数据权限配置类

@Configuration
public class MybatisConfig {

    @Bean
    public ConfigurationCustomizer mybatisConfigurationCustomizer(){
        return configuration -> configuration.setObjectWrapperFactory(new MapWrapperFactory());
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 1.添加数据权限插件
        interceptor.addInnerInterceptor(new DataPermissionInterceptor(new DataPressionConfig()));
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();

        // 分页插件
        paginationInnerInterceptor.setOptimizeJoin(false);
        paginationInnerInterceptor.setOverflow(true);
        paginationInnerInterceptor.setDbType(DbType.POSTGRE_SQL);
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}

     2. 数据权限拦截器
    @Slf4j
    @Component
    public class DataPressionConfig implements DataPermissionHandler {
        
        @Override
        public Expression getSqlSegment(Expression where, String mappedStatementId) {
    
            try {
                if(null==mappedStatementId){
                    return null;
                }
                Class<?> mapperClazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
                String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
    
    
                // 获取自身类中的所有方法,不包括继承。与访问权限无关
                Method[] methods = mapperClazz.getDeclaredMethods();
                for (Method method : methods) {
                    DataScope dataScopeAnnotationMethod = method.getAnnotation(DataScope.class);
                    if(null==dataScopeAnnotationMethod){
                        continue;
                    }
                    //spring aoc里拿方法参数
    
                    Parameter[] parameters= method.getParameters();
                    if (parameters.length > 0) {
                        log.info("方法参数:" +  dataScopeAnnotationMethod.oneselfScopeName() );
                    }
                    if (ObjectUtils.isEmpty(dataScopeAnnotationMethod) || !dataScopeAnnotationMethod.enabled()) {
                        continue;
                    }
                    if (method.getName().equals(methodName) || (method.getName() + "_COUNT").equals(methodName) || (method.getName() + "_count").equals(methodName)) {
                        return buildDataScopeByAnnotation(dataScopeAnnotationMethod,mappedStatementId);
                    }
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        /**
         * DataScope注解方式,拼装数据权限
         *
         * @param dataScope
         * @return
         */
        private Expression buildDataScopeByAnnotation(DataScope dataScope,String mapperId) {
            Map<String, Object> params = DataPermissionContext.getParams();
            if (params == null || params.isEmpty()) {
                return null;
            }
            Object areaCodes = params.get(mapperId);
            List<String> dataScopeDeptIds=  (List<String>) areaCodes;
    
            // 获取注解信息
            String tableAlias = dataScope.tableAlias();
    
            String areaCodes= dataScope.areaCodes();
            Expression expression = buildDataScopeExpression(tableAlias,   areaCodes, dataScopeDeptIds);
            return expression == null ? null : new Parenthesis(expression);
        }
    
        /**
         * 拼装数据权限
         *
         * @param tableAlias        表别名
         * @param oneselfScopeName  本人限制范围的字段名称
         * @param dataScopeDeptIds  数据权限部门ID集合,去重
         * @return
         */
        private Expression buildDataScopeExpression(String tableAlias,  String areaCodes, List<String> dataScopeDeptIds) {
            /**
             * 构造部门里行政区划 area_code 的in表达式。
             */
            try {
                String sql=tableAlias + "." + areaCodes+" in (";
                for(String areaCode:dataScopeDeptIds){
                    sql+="'"+areaCode+"',";
                }
                sql=sql.substring(0,sql.length()-1)+")";
                Expression selectExpression = CCJSqlParserUtil.parseCondExpression(sql, true);
                return selectExpression;
            } catch (JSQLParserException e) {
                throw new RuntimeException(e);
            }
    
        }
    
    
    
    }

    3. 存储spring aop切面拿到的数据

         

      public class DataPermissionContext {
      
          private static final ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<>();
      
          public static void setParams(Map<String, Object> params) {
              CONTEXT.set(params);
          }
      
          public static Map<String, Object> getParams() {
              return CONTEXT.get();
          }
      
          public static void clear() {
              CONTEXT.remove();
          }
      }

         4. 定义数据权限注解
        @Inherited
        @Target({ElementType.METHOD, ElementType.TYPE})
        @Retention(RetentionPolicy.RUNTIME)
        public @interface DataScope {
        
            /**
             * 是否生效,默认true-生效
             */
            boolean enabled() default true;
        
            /**
             * 表别名
             */
            String tableAlias() default "";
        
        
            /**
             * 本人限制范围的字段名称
             */
        
            String areaCodes() default "area_code";
        
        }
        5. 实现AOP切面
        @Slf4j
        @Aspect
        @Component
        public class DataAspet {
            @Pointcut("@annotation(DataScope)")
            public void logPoinCut() {
        
            }
        
        
            @Before("logPoinCut()")
            public void saveSysLog(JoinPoint joinPoint) {
        
                //从切面织入点处通过反射机制获取织入点处的方法
                MethodSignature signature = (MethodSignature) joinPoint.getSignature();
                //获取切入点所在的方法
                Method method = signature.getMethod();
                DataScope scope = method.getAnnotation(DataScope.class);
                Map<String, Object> paramValues = new HashMap<>();
                Object[] args = joinPoint.getArgs();
                Parameter[] parameters = method.getParameters();
                 if(null!=parameters&&parameters.length>0) {
                     //添加到最后一个参数
                     log.info("参数名称:{}",signature.getName()+":"+signature.getDeclaringTypeName());
                     paramValues.put(signature.getDeclaringTypeName()+"."+signature.getName(), args[parameters.length-1]);
                     DataPermissionContext.setParams(paramValues);
                 }else{
                     DataPermissionContext.setParams(null);
                 }
            }
        
        
        }

        6. mapper里注解使用数据权限

        tableAlias里你的sql里面要插入数据权限字段的表别名。

        比如:

        select count(*) as total,d.type as type from bus_device d group by d.type

        spring aop 会读取mapper方法最后一个参数,然后切入Sql变成

        select count(*) as total,d.type as type from bus_device d where

        d.area_code in(#{areaCodes} )  group by d.type

            @DataScope(tableAlias = "d")
            List<DeviceTypeCountDto> listByApplicationCategory(@Param(  @Param("type") String type,List<String> areaCodes);
        

        注意事项

        1. 性能考虑:数据权限过滤会增加SQL复杂度,可能影响查询性能,特别是对于大数据量表。可以考虑添加适当的索引优化。

        2. SQL注入风险:在拼接SQL时要特别注意防止SQL注入,建议使用预编译参数。

        3. 缓存问题:如果使用了缓存,需要注意数据权限可能导致缓存命中率下降或数据泄露问题。

        4. 多租户场景:在多租户系统中,数据权限通常需要与租户隔离一起考虑。

        5. 复杂查询:对于复杂的多表关联查询,数据权限条件可能需要更精细的控制。

        通过以上方案,我们可以灵活地在MyBatis-Plus中实现各种数据权限控制需求,根据项目实际情况选择最适合的实现方式。