基于mybatis拦截器实现的数据权限管理

4,725 阅读3分钟

前言:由于业务需要,因此就去研究了下实现思路,参考了很多文章,吸取了很多经验以及思路,于是写下该文章,总结下自己的学习成果,有不足的地方欢迎指出~

1、数据权限概念

数据无论拥有怎样的业务操作、定义,本质还是增删改查操作,所谓的数据权限,其实就是对增(INSERT)、删(DELETE)、改(UPDATE)、查(SELECT)进行权限控制。本文重点讲述下SELECT进行数据控制。

2、思路

数据权限还是一个挺复杂的逻辑,其实现也有多种,如下

  • 最简单的实现思路就是在业务接口中进行权限判断以及过滤,但是缺点也是显而易见的,代码侵入性强,通用性低。
  • SQL层面的抽象,改造底层SQL,实现数据权限的控制

因为项目中基本使用的都是mybatis,所以本次技术选型可以从mybatis拦截器中处理SQL,从而实现数据权限控制。

原SQL:

select * from coupon

处理后的SQL:

select id,name,create_user_id from (select * from coupon) scope where scope.create_user_id = ${当前登录者id}

这样该用户只能看到自己所创建的数据了,这样不仅能够实现行级数据的控制,同时也可以实现对列级别数据的控制。

3、原理

流程图

了解下mybatis结构图:

这里选择StatementHandlerprepare方法进行拦截,从而对sql进行封装。

/**
 * 分页拦截器
 */
@Setter
@Accessors(chain = true)
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class XiaozaoPaginationInterceptor extends PaginationInterceptor {

	/**
	 * 查询拦截器
	 */
	private QueryInterceptor[] queryInterceptors;

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		// 查询拦截器
		QueryInterceptorExecutor.exec(queryInterceptors, invocation);
		return super.intercept(invocation);
	}

}

/**
 * 查询拦截器执行器
 *
 * 目的:抽取此方法是为了后期方便同步更新 {@link XiaozaoPaginationInterceptor}
 */
class QueryInterceptorExecutor {

	/**
	 * 执行查询拦截器
	 *
	 * @param interceptors 拦截器
	 * @param invocation   拦截器参数
	 */
	static void exec(QueryInterceptor[] interceptors, Invocation invocation) throws Throwable {
		if (ObjectUtil.isEmpty(interceptors)) {
			return;
		}
		for (QueryInterceptor interceptor : interceptors) {
			interceptor.intercept(invocation);
		}
	}
}

MybtisPlusConfiguration注入bean的时候,对queryInterceptors赋值

	/**
	 * 分页拦截器
	 */
	@Bean
	public XiaozaoPaginationInterceptor paginationInterceptor(ObjectProvider<QueryInterceptor[]> queryInterceptors,
															  ObjectProvider<ISqlParser[]> sqlParsers,
															  ObjectProvider<ISqlParserFilter> sqlParserFilter) {
		XiaozaoPaginationInterceptor paginationInterceptor = new XiaozaoPaginationInterceptor();
		// 获取查询拦截器
		QueryInterceptor[] queryInterceptorArray = queryInterceptors.getIfAvailable();
		if (ObjectUtil.isNotEmpty(queryInterceptorArray)) {
			AnnotationAwareOrderComparator.sort(queryInterceptorArray);
			paginationInterceptor.setQueryInterceptors(queryInterceptorArray);
		}
		....
		return paginationInterceptor;
	}

重写intercept接口

/**
 * mybatis 数据权限拦截器
 */
@Slf4j
@RequiredArgsConstructor
public class DataScopeInterceptor implements QueryInterceptor {

	private ConcurrentMap<String, DataAuth> dataAuthMap = new ConcurrentHashMap<>(8);

	private final DataScopeHandler dataScopeHandler;
	private final DataScopeProperties dataScopeProperties;

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		// 未取到用户则放行
		...
		if (zaoUser == null) {
			return invocation.proceed();
		}
		// 接口没有开启数据权限则放行
		if (!Boolean.TRUE.equals(DataAuthContext.CONTEXT_HOLDER.get())) {
			return invocation.proceed();
		}

		StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
		MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

		// 非SELECT操作放行
		MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
		if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
			|| StatementType.CALLABLE == mappedStatement.getStatementType()) {
			return invocation.proceed();
		}

		BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
		String originalSql = boundSql.getSql();

		//查找注解中包含DataAuth类型的参数
		DataAuth dataAuth = findDataAuthAnnotation(mappedStatement);

		//注解为空并且数据权限方法名未匹配到,则放行
		....
        
		if (dataAuth == null && mapperSkip) {
			return invocation.proceed();
		}

		//创建数据权限模型
		DataScopeModel dataScope = new DataScopeModel();

		//若注解不为空,则配置注解项
		if (dataAuth != null) {
			...
		}

		// 封装Sql 获取数据权限规则对应的筛选Sql
		String sqlCondition = dataScopeHandler.sqlCondition(invocation, mapperId, dataScope, zaoUser, originalSql);
		if (!StringUtil.isBlank(sqlCondition)) {
			metaObject.setValue("delegate.boundSql.sql", sqlCondition);
		}
		return invocation.proceed();
	}

	/**
	 * 获取数据权限注解信息
	 *
	 * @param mappedStatement mappedStatement
	 * @return DataAuth
	 */
	private DataAuth findDataAuthAnnotation(MappedStatement mappedStatement) {
		...
    }

}

根据数据权限配置封装SQL

/**
 * 默认数据权限规则
 */
@Slf4j
@RequiredArgsConstructor
public class XiaozaoDataScopeHandler implements DataScopeHandler {

	private final ScopeModelHandler scopeModelHandler;

	@Override
	public String sqlCondition(Invocation invocation, String mapperId, DataScopeModel dataScope, ZaoUser zaoUser, String originalSql) {

		//数据权限资源编号
		String code = dataScope.getResourceCode();

		//根据mapperId从数据库中获取对应模型
		List<DataScopeModel> dataScopeDbs = scopeModelHandler.listDataScopeByMapper(mapperId, zaoUser.getRoleId());

		//mapperId配置未取到则从数据库中根据资源编号获取
		if (CollectionUtil.isEmpty(dataScopeDbs) && StringUtil.isNotBlank(code)) {
			dataScopeDbs = scopeModelHandler.listDataScopeByCode(code);
		}

		//未从数据库找到对应配置则采用默认
		List<DataScopeModel> scopes = Lists.newArrayList();
		if (CollectionUtil.isNotEmpty(dataScopeDbs)) {
			scopes.addAll(dataScopeDbs);
		} else {
			// 没查询到角色关联数据权限当全权处理
			return null;
		}
		String where = "where ";
		int index = 0;
		String whereSql = "scope.{} in ({})";
		for (DataScopeModel scope : scopes) {
			//判断数据权限类型并组装对应Sql
			Integer scopeRule = Objects.requireNonNull(scope).getScopeType();
			DataScopeEnum scopeTypeEnum = DataScopeEnum.of(scopeRule);
			List<Long> ids = new ArrayList<>();

			if (DataScopeEnum.ALL == scopeTypeEnum || StringUtil.containsAny(zaoUser.getRoleName(), RoleConstant.ADMINISTRATOR)) {
				return null;
			} else if (DataScopeEnum.CUSTOM == scopeTypeEnum) {
				whereSql = PlaceholderUtil.getDefaultResolver().resolveByMap(scope.getScopeValue(), BeanUtil.toMap(zaoUser));
			} else if (DataScopeEnum.OWN == scopeTypeEnum) {
				ids.add(zaoUser.getUserId());
			} else if (DataScopeEnum.OWN_DEPT == scopeTypeEnum) {
				ids.addAll(Func.toLongList(zaoUser.getDeptId()));
			} else if (DataScopeEnum.OWN_DEPT_CHILD == scopeTypeEnum) {
				...
			} else if (DataScopeEnum.OWN_DEPT_CUSTOM == scopeTypeEnum) {
               ...
            } else if (DataScopeEnum.OWN_DEPT_CHILD_CUSTOM == scopeTypeEnum) {
               ...
            }
			String tmpWhereSql = StringUtil.format(whereSql, scope.getScopeColumn(), StringUtil.join(ids));
			if (DataScopeEnum.CUSTOM_DATA_SCOPE_LIST.contains(scopeTypeEnum)) {
				tmpWhereSql = tmpWhereSql.replaceFirst("where", "");
			}
			if (index == 0) {
				where += " (" + tmpWhereSql + ") ";
			} else {
				where += " or (" + tmpWhereSql + ") ";
			}
			index ++;
		}
		String sql = StringUtil.format(" select {} from ({}) scope " + where, Func.toStr(dataScope.getScopeField(), "*"), originalSql);
		return sql;
	}

}

就这样对底层sql进行封装从而实现了数据权限的控制,但是同时它也有它的缺点。

4、缺点&问题

  • 性能问题:对于某些sql的封装可能需要用到in过滤、多表问题等。
  • 配置问题:对于配置规则的人员有点要求,需要了解这个原理。
  • 避免影响全局:采用注解形式+指定方法,只拦截指定接口中的某些个查询方法。
  • 业务变动导致mapper方法变动,则数据权限的配置也需要更新mapperId否则不生效。
  • 自定义sql的值需要关注业务sql中的select字段。
  • 对于一些统计接口(select sum(xx)),需要在代码中做特殊处理