前言:由于业务需要,因此就去研究了下实现思路,参考了很多文章,吸取了很多经验以及思路,于是写下该文章,总结下自己的学习成果,有不足的地方欢迎指出~
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结构图:
这里选择StatementHandler
的prepare
方法进行拦截,从而对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)),需要在代码中做特殊处理