网站首页 > 文章精选 正文
作者:京东科技 李俊龙
一、使用方式
数据权限控制需要对查询出的数据进行筛选,对业务入侵最少的方式就是利用mybatis或者数据库连接池的切片对已有业务的sql进行修改。切片逻辑完成后,仅需要在业务中加入少量标记代码,就可以实现对数据权限的控制。这种修改方式,对老业务的逻辑没有入侵或只有少量入侵,基本不影响老业务的逻辑和可读性;对新业务,业务开发人员无需过多关注权限问题,可以集中精力处理业务逻辑。
由于部门代码中使用的数据库连接池种类较多,不利于切片控制逻辑的快速完成,而sql拼接的部分基本只有mybatis和java字符串直接拼接两种方式,因此使用mybatis切片的方式来完成数据权限控制逻辑。在mybatis的mapper文件的接口上添加注解,注解中写明需要控制的权限种类、要控制的表名、列名即可控制接口的数据权限。
由于mybatis的mapper文件中的同一接口在多个地方被调用,有的需要控制数据权限,有的不需要,因此增加一种权限控制方式:通过ThreadLocal传递权限控制规则来控制当前sql执行时控制数据权限。
权限控制规则格式如下:
限权规则code1(表名1.字段名1,表名2.字段名2);限权规则code2(表名3.字段名3,表名4.字段名4)
例如:enterprise(channel.enterprise_code);account(table.column);channel(table3.id)
上下文传递工具类如下所示,使用回调的方式传递ThreadLocal可以防止使用者忘记清除上下文。
public class DataAuthContextUtil {
/**
* 不方便使用注解的地方,可以直接使用上下文设置数据规则
*/
private static ThreadLocal<String> useDataAuth = new ThreadLocal<>();
/**
* 有的sql只在部分情况下需要使用数据权限限制
* 上下文和注解中均可设置数据权限规则,都设置时,上下文中的优先
*
* @param supplier
*/
public static <T> T executeSqlWithDataAuthRule(String rule, Supplier<T> supplier) {
try {
useDataAuth.set(rule);
return supplier.get();
} finally {
useDataAuth.remove();
}
}
/**
* 获取数据权限标志
*
* @return
*/
public static String getUseDataAuthRule() {
return useDataAuth.get();
}
}
二、切片实现流程
三、其他技术细节
(1)在切面中获取原始sql
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import reactor.util.function.Tuple2;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
@Intercepts({
// @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
@Slf4j
public class DataAuthInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]);
String sql = boundSql.getSql();
} catch (Exception e) {
log.error("数据权限添加出错,当前sql未加数据权限限制!", e);
throw e;
}
return invocation.proceed();
}
}
(2)将权限项加入原始sql中
使用druid附带的ast解析功能修改sql,代码如下
/**
* 权限限制写入sql
*
* @param sql
* @param tableAuthMap key:table value1:column value2:values权限项
* @return
*/
public static StringBuilder addAuthLimitToSql(String sql, Map<String, Tuple2<String, Set<String>>> tableAuthMap) {
List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, "mysql");
StringBuilder authSql = new StringBuilder();
for (SQLStatement stmt : stmtList) {
stmt.accept(new MySqlASTVisitorAdapter() {
@Override
public boolean visit(MySqlSelectQueryBlock x) {
SQLTableSource from = x.getFrom();
Set<String> tableList = new HashSet<>();
getTableList(from, tableList);
for (String tableName : tableList) {
if (tableAuthMap.containsKey(tableName)) {
x.addCondition(tableName + "in (...略)");
}
}
return true;
}
});
authSql.append(stmt);
}
return authSql;
}
private static void getTableList(SQLTableSource from, Set<String> tableList) {
if (from instanceof SQLExprTableSource) {
SQLExprTableSource tableSource = (SQLExprTableSource) from;
String name = tableSource.getTableName().replace("`", "");
tableList.add(name);
String alias = tableSource.getAlias();
if (StringUtils.isNotBlank(alias)) {
tableList.add(alias.replace("`", ""));
}
} else if (from instanceof SQLJoinTableSource) {
SQLJoinTableSource joinTableSource = (SQLJoinTableSource) from;
getTableList(joinTableSource.getLeft(), tableList);
getTableList(joinTableSource.getRight(), tableList);
} else if (from instanceof SQLSubqueryTableSource) {
SQLSubqueryTableSource tableSource = (SQLSubqueryTableSource) from;
tableList.add(tableSource.getAlias().replace("`", ""));
} else if (from instanceof SQLLateralViewTableSource) {
log.warn("SQLLateralView不用处理");
} else if (from instanceof SQLUnionQueryTableSource) {
//union 不需要处理
log.warn("union不用处理");
} else if (from instanceof SQLUnnestTableSource) {
log.warn("Unnest不用处理");
} else if (from instanceof SQLValuesTableSource) {
log.warn("Values不用处理");
} else if (from instanceof SQLWithSubqueryClause) {
log.warn("子查询不用处理");
} else if (from instanceof SQLTableSourceImpl) {
log.warn("Impl不用处理");
}
}
}
(3)将修改过后的sql写回mybatis
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);
// 组装 MappedStatement
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), new MySqlSource(boundSql), ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
MappedStatement newMappedStatement = builder.build();
MetaObject metaObject = MetaObject.forObject(newMappedStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
metaObject.setValue("sqlSource.boundSql.sql", newSql);
invocation.getArgs()[0] = newMappedStatement;
参考文章:
https://blog.csdn.net/e_anjing/article/details/79102693
猜你喜欢
- 2025-06-09 PageHelper - 最方便的 MyBatis 分页插件
- 2025-06-09 50个Java编程技巧,免费送给大家(java编程基础知识入门)
- 2025-06-09 SpringBoot 各种分页查询方式详解(全网最全)
- 2025-06-09 SpringBatch - R&W, 我与富婆的这一年
- 2025-06-09 面试官:说说MyBatis分页插件(PageHelper)工作原理和配置过程?
- 2025-06-09 面试二:pagehelper是怎么实现分页的,
- 2025-06-09 SpringBoot集成Mybatis-Plus分页插件
- 2025-06-09 【开发技术】Mybatis中进行多表关联查询?性能是不是会变好呢?
- 2025-06-09 每天从外包系统同步百万数据,用什么方案?Java实战讲解
- 2025-06-09 MyBatis插件开发实战:手写一个分页插件
- 最近发表
-
- 面试中常被问到的Hash表,你了解吗
- JAVA面试考点:一文搞懂一致性Hash的原理和实现
- 一次性搞清楚equals和hashCode(hashcode() 与equals()区别,简单说明)
- HashMap.Key的故事:Key为什么出现Hash碰撞及冲突呢?
- hash冲突的几种解决方案对比(hash冲突的解决方式)
- 游戏王LN 无头骑士(无头骑士cv)
- Linux ln、unlink命令用法(linux link命令详解)
- n和l分不清矫正发音方法,这三步就够了
- golang引用私有gitlab项目代码(golang引入当前包下的文件)
- Instamic:录音领域中的 GoPro,让你想录就录,随心所欲
- 标签列表
-
- newcoder (56)
- 字符串的长度是指 (45)
- drawcontours()参数说明 (60)
- unsignedshortint (59)
- postman并发请求 (47)
- python列表删除 (50)
- 左程云什么水平 (56)
- 计算机网络的拓扑结构是指() (45)
- 编程题 (64)
- postgresql默认端口 (66)
- 数据库的概念模型独立于 (48)
- 产生系统死锁的原因可能是由于 (51)
- 数据库中只存放视图的 (62)
- 在vi中退出不保存的命令是 (53)
- 哪个命令可以将普通用户转换成超级用户 (49)
- noscript标签的作用 (48)
- 联合利华网申 (49)
- swagger和postman (46)
- 结构化程序设计主要强调 (53)
- 172.1 (57)
- apipostwebsocket (47)
- 唯品会后台 (61)
- 简历助手 (56)
- offshow (61)
- mysql数据库面试题 (57)