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


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 环境准备

依赖配置
<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.5.3.1version>
dependency>
<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-extensionartifactId>
    <version>3.5.3.1version>
dependency>

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<String> blackList;

	public List<String> getBlackList() {
		return blackList;
	}

	public void setBlackList(List<String> blackList) {
		this.blackList= blackList;
	}
	public Boolean not(String url) {
		List<String> 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 最后执行,确保 LIMITCOUNT 基于最终 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<UserContext> 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<User> selectCustomPage(IPage<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
    

4.2 LIMIT 子句丢失

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

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

4.3 多表关联查询权限失效

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

  1. 在条件中指定表别名:
    String condition = "u.dept_id = 2"; // u 为 user 表别名
    
  2. 使用 SQL 解析器动态处理别名。

你可能感兴趣的:(spring,boot,mybatis)