Spring Boot + MyBatis-Plus 插件(数据权限实战)

06-01 1677阅读

Spring Boot + MyBatis-Plus 插件(数据权限实战)

一、数据权限概念

1.1 什么是数据权限?

数据权限(Data Permission)是系统根据用户角色、部门或其他业务规则动态控制数据访问范围的技术。其核心目标是确保用户仅能访问其权限范围内的数据,避免越权操作。常见场景包括:

  • 多租户隔离:不同租户的数据物理或逻辑隔离。
  • 部门数据管控:用户仅能查看本部门或下属部门的数据。
  • 角色权限分级:根据角色级别限制数据可见性(如普通员工与经理的视图差异)。

    二、MyBatis-Plus 数据权限实现原理

    2.1 DataPermissionHandler 工作机制

    DataPermissionHandler 是 MyBatis-Plus 提供的接口,用于在 SQL 解析阶段动态注入权限条件。其核心流程如下:

    1. SQL 解析:MyBatis-Plus 解析 Mapper 方法生成的 SQL 语句。
    2. 条件注入:调用 getSqlSegment 方法,将自定义条件拼接到原始 WHERE 子句。
    3. 执行优化:处理后的 SQL 传递至后续插件(如分页插件),生成最终执行语句。

    三、完整实现步骤

    3.1 环境准备

    依赖配置
        com.baomidou
        mybatis-plus-boot-starter
        3.5.3.1
    
    
        com.baomidou
        mybatis-plus-extension
        3.5.3.1
    
    

    3.2 自定义数据权限处理器

    3.2.1自定义注解
    package cn.sdlnrj.foundation.common.annotation;
    import java.lang.annotation.*;
    @Inherited
    @Target({ ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface CustomDataPermission {
    	/**
    	 * 是否进行数据权限
    	 */
    	public boolean isRole() default false;
    	/**
    	* 权限字段
    	*/
    	public String permissionField() default "";
    	/**
    	* 权限方式(根据 用户、角色、部门等)
    	*/
    	public PermissionTypeEnum permissionType() default PermissionTypeEnum.USER;
    }
    
    3.2.2数据权限获取的具体实现方法
    @Getter
    @AllArgsConstructor
    public enum PermissionTypeEnum {
    	USER("用户", 10) {
    		/**
    		 * 获取当前用户创建的数据
    		 * @param classAnnotation
    		 * @return
    		 */
    		@Override
    		public Expression getPermission(CustomDataPermission classAnnotation) {
    			//1.查询判断当前用户的角色,比如管理员角色不需要拼接数据权限
    			EqualsTo equalsTo = new EqualsTo();
    			equalsTo.setRightExpression(new StringValue("获取当前登录用户账号"));
    			if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) {
    				equalsTo.setLeftExpression(new Column(classAnnotation.permissionField()));
    			}
    			return equalsTo;
    		}
    	}, DEPT("部门", 20) {
    		@Override
    		public Expression getPermission(CustomDataPermission classAnnotation) {
    			EqualsTo equalsTo = new EqualsTo();
    			//获取当前用户的部门权限
    			equalsTo.setRightExpression(new StringValue("获取当前用户的部门权限"));
    			if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) {
    				equalsTo.setLeftExpression(new Column(classAnnotation.permissionField()));
    			}
    			return equalsTo;
    		}
    	}, ROLE("角色", 30) {
    		@Override
    		public Expression getPermission(CustomDataPermission classAnnotation) {
    			EqualsTo equalsTo = new EqualsTo();
    			equalsTo.setRightExpression(new StringValue("获取当前用户的角色权限"));
    			if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) {
    				equalsTo.setLeftExpression(new Column(classAnnotation.permissionField()));
    			}
    			return equalsTo;
    		}
    	}, POSITION("职务", 40) {
    		@Override
    		public Expression getPermission(CustomDataPermission classAnnotation) {
    			EqualsTo equalsTo = new EqualsTo();
    			equalsTo.setRightExpression(new StringValue("获取当前用户的角色权限"));
    			if (Objects.nonNull(classAnnotation) && StringUtils.isNotBlank(classAnnotation.permissionField())) {
    				equalsTo.setLeftExpression(new Column(classAnnotation.permissionField()));
    			}
    			return equalsTo;
    		}
    	};
    	final String name;
    	final Integer code;
    	public abstract Expression getPermission(CustomDataPermission classAnnotation);
    	public static Expression handle(CustomDataPermission classAnnotation) {
    		PermissionTypeEnum[] values = PermissionTypeEnum.values();
    		for (PermissionTypeEnum emissionStatus : values) {
    			if(emissionStatus.equals(classAnnotation.permissionType())){
    				return emissionStatus.getPermission(classAnnotation);
    			}
    		}
    		return new EqualsTo();
    	}
    }
    

    根据自己业务需求,可增加权限黑名单,也可增加白名单,这里不做白名单实例了。

    data-permissions:
      blackList: 
      	-接口地址
    
    @Configuration
    @ConfigurationProperties(prefix = "data-permissions")
    public class WhiteListConfig {
    	private List blackList;
    	public List getBlackList() {
    		return blackList;
    	}
    	public void setBlackList(List blackList) {
    		this.blackList= blackList;
    	}
    	public Boolean not(String url) {
    		List collect = blackList.stream().filter(white -> url.contains(white)).collect(Collectors.toList());
    		if (CollectionUtils.isEmpty(collect)) {
    			return false;
    		}
    		return true;
    	}
    }
    
    @Slf4j
    public class MyDataPermissionHandler implements DataPermissionHandler {
    	/**
    	 * @param where             原SQL Where 条件表达式
    	 * @param mappedStatementId Mapper接口方法ID
    	 * @return
    	 */
    	@SneakyThrows
    	@Override
    	public Expression getSqlSegment(Expression where, String mappedStatementId) {
    		// 获取当前用户权限上下文(示例:部门ID和租户ID)
            UserContext user = UserContextHolder.getCurrentUser();
    		//如果调用mybatisplus查询单一的sql,则不进行数据权限拼接。
    		Class clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
    		if (StringUtils.isNotBlank(mappedStatementId)) {
    			String substring = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
    			if (substring.equals("selectById")) {
    				return where;
    			}
    		}
    		//判断访问的url是否在黑名单中
    		boolean isPreventAddress = SpringUtil.getBean(WhiteListConfig.class).not(WebUtil.getRequest().getRequestURI());
    		if (isPreventAddress) {
    			return where;
    		}
    		//判断mapper上是否有注解,CustomDataPermission是用在mapper上,标识操作sql语句的别名,以及mapper是否需要走数据权限。
    		CustomDataPermission classAnnotation = clazz.getAnnotation(CustomDataPermission.class);
    		if (Objects.isNull(classAnnotation)) {
    			return where;
    		}
    		// 创建 AND 表达式 拼接Where 和 = 表达式
    		return new AndExpression(where, PermissionTypeEnum.handle(classAnnotation));
    	}
    }
    

    3.3 插件配置与顺序控制

    3.3.1. 多插件共存时的顺序规则
    插件类型推荐顺序说明
    数据权限插件1确保 WHERE 条件在所有分页、租户逻辑之前生效。
    多租户插件2若同时使用多租户插件,应放在数据权限插件之后。
    分页插件3最后执行,确保 LIMIT 和 COUNT 基于最终 SQL。
    乐观锁插件4不影响 SQL 结构,顺序无严格要求。

    完整配置示例:

    @Configuration
    public class MyBatisPlusConfig {
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            
            // 1. 数据权限插件
        	interceptor.addInnerInterceptor(new DataPermissionInterceptor(...));
        
        	// 2. 多租户插件
        	interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(...));
        
        	// 3. 分页插件
        	interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        
        	// 4. 乐观锁插件
        	interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
            
            return interceptor;
        }
    }
    

    3.4 权限上下文管理

    public class UserContextHolder {
        private static final TransmittableThreadLocal contextHolder = new TransmittableThreadLocal();
        public static void setUserContext(UserContext user) {
            contextHolder.set(user);
        }
        public static UserContext getCurrentUser() {
            return contextHolder.get();
        }
        public static void clear() {
            contextHolder.remove();
        }
    }
    // 示例拦截器设置上下文
    @Component
    public class AuthInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            UserContext user = extractUser(request);
            UserContextHolder.setUserContext(user);
            return true;
        }
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
            UserContextHolder.clear();
        }
    }
    

    四、常见问题与解决方案

    4.1 分页总数不准确

    问题原因:COUNT 查询未应用数据权限条件。

    解决方案:

    1. 检查插件执行顺序,确保数据权限插件在分页插件之前。
    2. 手动指定 COUNT 查询:
      @Select("SELECT * FROM user WHERE ${ew.customSqlSegment}")
      @Select("SELECT COUNT(*) FROM user WHERE ${ew.customSqlSegment}")
      IPage selectCustomPage(IPage page, @Param(Constants.WRAPPER) Wrapper wrapper);
      

    4.2 LIMIT 子句丢失

    问题原因:分页插件未正确配置或执行顺序错误。

    解决方案:

    1. 确保 PaginationInnerInterceptor 已添加到拦截器链。
    2. 验证插件顺序:数据权限插件 → 分页插件。

    4.3 多表关联查询权限失效

    问题原因:权限条件未正确附加到关联表。

    解决方案:

    1. 在条件中指定表别名:
      String condition = "u.dept_id = 2"; // u 为 user 表别名
      
    2. 使用 SQL 解析器动态处理别名。
    Spring Boot + MyBatis-Plus 插件(数据权限实战)
    (图片来源网络,侵删)
    Spring Boot + MyBatis-Plus 插件(数据权限实战)
    (图片来源网络,侵删)
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码