MyBatis-Plus数据权限插件的简单使用

 更新时间:2024年10月31日 11:17:16   作者:Charge8  
在MyBatis-Plus中,通过DataPermissionInterceptor插件实现数据权限控制,首先需要创建自定义注解和处理类,利用JSQLParser库动态修改SQL,实现按角色权限过滤数据,配置类中注册拦截器,确保只有授权用户能访问指定数据,感兴趣的可以了解一下

平时开发中遇到根据当前用户的角色,只能查看数据权限范围的数据需求。一般我们采用拦截器在mybatis执行sql前修改语句,限定where范围。

通常拦截器针对注解进行识别,可以更好的对需要的接口进行拦截和转化。

一般步骤如下:

  • 创建注解类
  • 创建处理类,获取数据权限 SQL 片段,设置 where条件
  • 将拦截器加到MyBatis-Plus插件中

一、数据权限插件简介

官方文档-数据权限插件:https://baomidou.com/plugins/data-permission/

DataPermissionInterceptor 是 MyBatis-Plus 提供的一个插件,用于实现数据权限控制。它通过拦截执行的 SQL 语句,并动态拼接权限相关的 SQL 片段,来实现对用户数据访问的控制。

DataPermissionInterceptor 的工作原理会在 SQL 执行前拦截 SQL 语句,并根据用户权限动态添加权限相关的 SQL 片段。这样,只有用户有权限访问的数据才会被查询出来。

JSQLParser 是一个开源的 SQL 解析库,可方便地解析和修改 SQL 语句。它是插件实现权限逻辑的关键工具,MyBatis-Plus 的数据权限依托于 JSQLParser 的解析能力。

使用方法:

  • 自定义 MultiDataPermissionHandler的实现处理器类,处理自定义数据权限逻辑。
  • 注册数据权限拦截器

二、数据权限插件实现

在项目中,使用的是 spring security + oauth2安全认证,角色来定义数据权限类型。

角色的数据权限类型:

@Getter
@RequiredArgsConstructor
public enum DataScopeEnum {

	/**
	 * 全部数据权限
	 */
	DATA_SCOPE_ALL("0", "全部数据权限"),

	/**
	 * 自定义数据权限
	 */
	DATA_SCOPE_CUSTOM("1", "自定义数据权限"),

	/**
	 * 本部门及子级数据权限
	 */
	DATA_SCOPE_DEPT_AND_CHILD("2", "部门及子级数据权限"),

	/**
	 * 本部门数据权限
	 */
	DATA_SCOPE_DEPT("3", "部门数据权限"),

	/**
	 * 本人数据权限
	 */
	DATA_SCOPE_SELF("4", "本人数据权限"),
	;

	/**
	 * 对应数据库字典值
	 */
	private final String dbValue;

	/**
	 * 描述
	 */
	private final String description;

}

对于 UserDetails用户信息我们做了扩展。添加了关于数据权限的字段信息。

public class SxdhcloudUser extends User implements OAuth2AuthenticatedPrincipal {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	/**
	 * 扩展属性,方便存放oauth 上下文相关信息
	 */
	private final Map<String, Object> attributes = new HashMap<>();

	/**
	 * 租户ID
	 */
	@Getter
	@JsonSerialize(using = ToStringSerializer.class)
	private final Long tenantId;

	/**
	 * 用户ID
	 */
	@Getter
	@JsonSerialize(using = ToStringSerializer.class)
	private final Long id;

	/**
	 * 部门ID
	 */
	@Getter
	@JsonSerialize(using = ToStringSerializer.class)
	private final Long deptId;

	/**
	 * 手机号
	 */
	@Getter
	private final String phone;


	/**
	 * 角色数据权限类型,去重
	 */
	@Getter
	private final Set<String> dataScopeTypes;

	/**
	 * 数据权限部门ID集合,去重
	 */
	@Getter
	private final Set<Long> dataScopeDeptIds;

	/**
	 * 数据权限本人ID
	 */
	@Getter
	private final Long dataScopeCreateId;
	
}

用户在登录认证成功之后,获取到角色数据权限的相关数据,并放到 UserDetails用户信息中。

1、自定义注解

自定义数据权限注解。

/**
 * 数据权限注解。
 * 可以使用在类上,也可以使用在方法上。
 * - 如果 Mapper类加上注解,表示 Mapper提供的方法以及自定义的方法都会被加上数据权限
 * - 如果 Mapper类的方法加在上注解,表示该方法会被加上数据权限
 * - 如果 Mapper类和其方法同时加上注解,优先级为:【类上 > 方法上】
 * - 如果不需要数据权限,可以不加注解,也可以使用 @DataScope(enabled = false)
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {

	/**
	 * 是否生效,默认true-生效
	 */
	boolean enabled() default true;

	/**
	 * 表别名
	 */
	String tableAlias() default "";

	/**
	 * 部门限制范围的字段名称
	 */
	String deptScopeName() default "dept_id";

	/**
	 * 本人限制范围的字段名称
	 */
	String oneselfScopeName() default "create_id";

}

2、自定义处理器

自定义处理器类并实现 MultiDataPermissionHandler接口,在 getSqlSegment()方法中处理自定义数据权限逻辑。

注意:mybaits-plus 必须大于 3.5.2版本。

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.handler.MultiDataPermissionHandler;
import com.sxdh.sxdhcloud.common.mybatis.annotation.DataScope;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 数据权限拼装逻辑处理
 *
 */
public class DataScopeHandler implements MultiDataPermissionHandler {

	/**
	 * 获取数据权限 SQL 片段。
	 * <p>旧的 {@link MultiDataPermissionHandler#getSqlSegment(Expression, String)} 方法第一个参数包含所有的 where 条件信息,如果 return 了 null 会覆盖原有的 where 数据,</p>
	 * <p>新版的 {@link MultiDataPermissionHandler#getSqlSegment(Table, Expression, String)} 方法不能覆盖原有的 where 数据,如果 return 了 null 则表示不追加任何 where 条件</p>
	 *
	 * @param table             所执行的数据库表信息,可以通过此参数获取表名和表别名
	 * @param where             原有的 where 条件信息
	 * @param mappedStatementId Mybatis MappedStatement Id 根据该参数可以判断具体执行方法
	 * @return JSqlParser 条件表达式,返回的条件表达式会拼接在原有的表达式后面(不会覆盖原有的表达式)
	 */
	@Override
	public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
		try {
			Class<?> mapperClazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
			String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);

			/**
			 * DataScope注解优先级:【类上 > 方法上】
			 */
			// 获取 DataScope注解
			DataScope dataScopeAnnotationClazz = mapperClazz.getAnnotation(DataScope.class);
			if (ObjectUtils.isNotEmpty(dataScopeAnnotationClazz) && dataScopeAnnotationClazz.enabled()) {
				return buildDataScopeByAnnotation(dataScopeAnnotationClazz);
			}
			// 获取自身类中的所有方法,不包括继承。与访问权限无关
			Method[] methods = mapperClazz.getDeclaredMethods();
			for (Method method : methods) {
				DataScope dataScopeAnnotationMethod = method.getAnnotation(DataScope.class);
				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);
				}
			}
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * DataScope注解方式,拼装数据权限
	 *
	 * @param dataScope
	 * @return
	 */
	private Expression buildDataScopeByAnnotation(DataScope dataScope) {
		// 获取 UserDetails用户信息
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication == null) {
			return null;
		}
		Map<String, Object> userDetailsMap = BeanUtil.beanToMap(authentication.getPrincipal());
		Set<String> dataScopeTypes = (Set<String>) userDetailsMap.get("dataScopeTypes");
		Set<Long> dataScopeDeptIds = (Set<Long>) userDetailsMap.get("dataScopeDeptIds");
		Long dataScopeCreateId = (Long) userDetailsMap.get("dataScopeCreateId");

		// 获取注解信息
		String tableAlias = dataScope.tableAlias();
		String deptScopeName = dataScope.deptScopeName();
		String oneselfScopeName = dataScope.oneselfScopeName();
		Expression expression = buildDataScopeExpression(tableAlias, deptScopeName, oneselfScopeName, dataScopeDeptIds, dataScopeCreateId);
		return expression == null ? null : new Parenthesis(expression);
	}

	/**
	 * 拼装数据权限
	 *
	 * @param tableAlias        表别名
	 * @param deptScopeName     部门限制范围的字段名称
	 * @param oneselfScopeName  本人限制范围的字段名称
	 * @param dataScopeDeptIds  数据权限部门ID集合,去重
	 * @param dataScopeCreateId 数据权限本人ID
	 * @return
	 */
	private Expression buildDataScopeExpression(String tableAlias, String deptScopeName, String oneselfScopeName, Set<Long> dataScopeDeptIds, Long dataScopeCreateId) {
		/**
		 * 构造部门in表达式。
		 */
		InExpression deptIdInExpression = null;
		if (CollectionUtils.isNotEmpty(dataScopeDeptIds)) {
			deptIdInExpression = new InExpression();
			ExpressionList deptIds = new ExpressionList(dataScopeDeptIds.stream().map(LongValue::new).collect(Collectors.toList()));
			// 设置左边的字段表达式,右边设置值。
			deptIdInExpression.setLeftExpression(buildColumn(tableAlias, deptScopeName));
			deptIdInExpression.setRightExpression(new Parenthesis(deptIds));
		}

		/**
		 * 构造本人eq表达式
		 */
		EqualsTo oneselfEqualsTo = null;
		if (dataScopeCreateId != null) {
			oneselfEqualsTo = new EqualsTo();
			oneselfEqualsTo.withLeftExpression(buildColumn(tableAlias, oneselfScopeName));
			oneselfEqualsTo.setRightExpression(new LongValue(dataScopeCreateId));
		}

		if (deptIdInExpression != null && oneselfEqualsTo != null) {
			return new OrExpression(deptIdInExpression, oneselfEqualsTo);
		} else if (deptIdInExpression != null && oneselfEqualsTo == null) {
			return deptIdInExpression;
		} else if (deptIdInExpression == null && oneselfEqualsTo != null) {
			return oneselfEqualsTo;
		}
		return null;
	}

	/**
	 * 构建Column
	 *
	 * @param tableAlias 表别名
	 * @param columnName 字段名称
	 * @return 带表别名字段
	 */
	public static Column buildColumn(String tableAlias, String columnName) {
		if (StringUtils.isNotEmpty(tableAlias)) {
			columnName = tableAlias + "." + columnName;
		}
		return new Column(columnName);
	}

}

3、注册数据权限拦截器

MybatisPlusConfig配置类中将自定义的处理器注册到 DataPermissionInterceptor 中。并将拦截器加到MyBatis-Plus插件中。

	@Bean
	public MybatisPlusInterceptor mybatisPlusInterceptor() {
		MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
		// 1.添加数据权限插件
		interceptor.addInnerInterceptor(new DataPermissionInterceptor(new DataScopeHandler()));
		// 2.添加分页插件
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
        // 设置数据库方言类型
        pageInterceptor.setDbType(DbType.MYSQL);
        // 下面配置根据需求自行设置
        // 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false
        pageInterceptor.setOverflow(false);
        // 单页分页条数限制,默认无限制
        pageInterceptor.setMaxLimit(500L);
        interceptor.addInnerInterceptor(pageInterceptor);
		return interceptor;
	}

4、注解使用

@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {

	/**
	 * 通过用户名查询用户信息(含有角色信息)
	 *
	 * @param username 用户名
	 * @return userVo
	 */
	UserVO getUserVoByUsername(String username);

	/**
	 * 分页查询用户信息(含角色)
	 *
	 * @param page      分页
	 * @param userDTO   查询参数
	 * @return list
	 */
	@DataScope(tableAlias = "u")
	IPage<UserVO> getUserVosPage(Page page, @Param("query") UserDTO userDTO, @Param("userIds") List<Long> userIds);

}

代码中的注释写的挺清楚,大家自行理解。

参考文章:

到此这篇关于MyBatis-Plus数据权限插件的简单使用的文章就介绍到这了,更多相关MyBatis-Plus数据权限内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Java中的ThreadPoolExecutor线程池原理细节解析

    Java中的ThreadPoolExecutor线程池原理细节解析

    这篇文章主要介绍了Java中的ThreadPoolExecutor线程池原理细节解析,ThreadPoolExecutor是一个线程池,最多可使用7个参数来控制线程池的生成,使用线程池可以避免创建和销毁线程的资源损耗,提高响应速度,并且可以管理线程池中线程的数量和状态等等,需要的朋友可以参考下
    2023-12-12
  • SpringBoot+easypoi实现数据的Excel导出

    SpringBoot+easypoi实现数据的Excel导出

    这篇文章主要为大家详细介绍了SpringBoot+easypoi实现数据的Excel导出,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-05-05
  • spring ioc的简单实例及bean的作用域属性解析

    spring ioc的简单实例及bean的作用域属性解析

    这篇文章主要介绍了spring ioc的简单实例及bean的作用域属性解析,具有一定借鉴价值,需要的朋友可以参考下
    2017-12-12
  • spring-data-redis自定义实现看门狗机制

    spring-data-redis自定义实现看门狗机制

    redission看门狗机制是解决分布式锁的续约问题,本文主要介绍了spring-data-redis自定义实现看门狗机制,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • Java中常用输出方式(print() println() printf())

    Java中常用输出方式(print() println() printf())

    这篇文章主要介绍了Java中常用输出方式(print() println() printf()),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • Java编程中最基础的文件和目录操作方法详解

    Java编程中最基础的文件和目录操作方法详解

    这篇文章主要介绍了Java编程中最基础的文件和目录操作方法详解,是Java入门学习中的基础知识,需要的朋友可以参考下
    2015-11-11
  • spring @Primary-在spring中的使用方式

    spring @Primary-在spring中的使用方式

    这篇文章主要介绍了spring @Primary-在spring中的使用方式,具有很好的参考价值,希望对大家有所帮助。
    2022-01-01
  • mybatis-generator生成文件覆盖问题的解决

    mybatis-generator生成文件覆盖问题的解决

    这篇文章主要介绍了mybatis-generator生成文件覆盖问题的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • Java创建并运行线程的方法

    Java创建并运行线程的方法

    这篇文章主要介绍了Java创建并运行线程的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • 解读CompletableFuture的底层原理

    解读CompletableFuture的底层原理

    本文探讨了Java8中CompletableFuture的原理和应用,详解其异步编程能力、工作机制及实际使用方法,CompletableFuture通过链式调用和状态管理优化异步任务,提高Java应用的效率和性能
    2024-09-09

最新评论