ThinkBridge 当前的数据权限仅对 MyBatis 的数据查询方式进行了支持。
MyBatis 集成
本页详细介绍 ThinkBridge 数据权限模块与 MyBatis 的集成细节,包括拦截器机制、SQL 解析流程、SQL 转换示例、性能优化以及高级自定义配置。
仓库坐标
接口规范
package com.luamas.thinkbridge.common.mapper.datascope;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
public interface DataScope {
/
* 数据所对应的资源
* @return 资源标识
/
String getResource();
/
* 判断当前数据权限范围是否需要管理此表
* @param tableName 当前需要处理的表名
* @return 如果当前数据权限范围包含当前表名,则返回 true。,否则返回 false
*/
boolean includes(String tableName);
/*
* 根据表名和表别名,动态生成的 where/or 筛选条件
* @param tableName 表名
* @param tableAlias 表别名,可能为空
* @return 数据规则表达式
*/
Expression getExpression(String tableName, Alias tableAlias);
}
执行流程
当 MyBatis 执行 Mapper 方法时,数据权限通过多层协作实现权限过滤,整体流程如下:
-
用户调用 Mapper 方法
-
AOP 切面拦截方法,检查是否有 @DataPermission 注解,若存在则解析注解并推入 DataPermissionRuleHolder 栈
-
MyBatis 生成 SQL 语句
-
DataPermissionInterceptor 拦截 StatementHandler.prepare() 方法
-
调用 DataPermissionHandler.filterDataScopes(mappedStatementId) 获取当前需要控制的 DataScope 列表
-
从 DataPermissionRuleHolder 中弹出当前规则
-
根据规则的 ignore/includeResources/excludeResources 过滤全局 DataScope
-
-
调用 DataPermissionHandler.ignorePermissionControl() 判断是否跳过处理(缓存命中时跳过)
-
遍历 DataScope 列表,调用 DataScopeSqlProcessor.parserSingle()(SELECT) 或 parserMulti()(INSERT/UPDATE/DELETE)
-
解析 SQL 的 FROM 子句涉及的每张表
-
对每张表调用 DataScope 的 includes(tableName) 判断是否需要处理
-
对匹配的表调用 getExpression() 获取过滤表达式
-
将所有表达式使用 AND 连接,注入到 SQL 的 WHERE 子句中
-
-
若当前 sql 未命中任何 DataScope,则记录到 MappedStatementIdsWithoutDataScope 中,下次直接跳过
-
执行改写后的 SQL
SQL 转换示例
单表查询
原 SQL:
select * from entity where id = ?
转换后:
SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1
多表 JOIN
原 SQL:
SELECT * FROM entity e
RIGHT JOIN entity1 e1 ON e1.id = e.id
转换后:
SELECT * FROM entity e
RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1
WHERE e1.tenant_id = 1
| 对于 RIGHT JOIN,左表的条件注入到 JOIN 的 ON 中,右表(驱动表)的条件注入到 WHERE 中。 |
嵌套子查询
原 SQL:
SELECT * FROM entity e
WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)
转换后:
SELECT * FROM entity e
WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)
AND e.tenant_id = 1
| 子查询和内层 SQL 都会被递归处理,每层各自注入对应的过滤条件。 |
隐式内连接
原 SQL:
SELECT * FROM entity, entity1 WHERE entity.id = entity1.id
转换后:
SELECT * FROM entity, entity1
WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1
高级 DataScope 实现
下面展示几种不同维度的 DataScope 实现模式,实际使用时可根据业务需求组合。
学校维度
@Component
public class SchoolDataScope implements DataScope {
public static final String RESOURCE_NAME = "school";
final String columnId = "school_name";
@Override
public String getResource() {
return RESOURCE_NAME;
}
@Override
public boolean includes(String tableName) {
// 利用表名前缀匹配
return tableName.toLowerCase().startsWith("h2student");
}
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
LoginUser loginUser = LoginUserHolder.get();
// 用户未登录或没有权限时,1=2 永假
if (loginUser == null || noSchoolPermission(loginUser)) {
return new EqualsTo(new LongValue(1), new LongValue(2));
}
// 学生维度由 StudentDataScope 处理
if (loginUser.isStudent()) {
return null; // 返回 null 表示不干预
}
// 生成 IN 条件:school_name IN ('实验中学', '德高中学')
List<Expression> list = loginUser.getSchoolNameList()
.stream()
.map(StringValue::new)
.collect(Collectors.toList());
Column column = new Column(tableAlias == null
? this.columnId
: tableAlias.getName() + "." + this.columnId);
ParenthesedExpressionList expressionList = new ParenthesedExpressionList();
expressionList.withExpressions(list);
return new InExpression(column, expressionList);
}
}
班级维度
@Component
public class ClassDataScope implements DataScope {
public static final String RESOURCE_NAME = "class";
final String columnId = "class_name";
// 使用 TreeSet(CASE_INSENSITIVE_ORDER) 忽略表名大小写
private final Set<String> tableNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
public ClassDataScope() {
this.tableNames.addAll(Arrays.asList("h2student", "h2teacher"));
}
@Override
public String getResource() {
return RESOURCE_NAME;
}
@Override
public boolean includes(String tableName) {
return this.tableNames.contains(tableName);
}
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
LoginUser loginUser = LoginUserHolder.get();
if (loginUser == null || noClassPermission(loginUser)) {
return new EqualsTo(new LongValue(1), new LongValue(2));
}
if (loginUser.isStudent()) {
return null; // 学生维度由 StudentDataScope 处理
}
// 生成 IN 条件:class_name IN ('一班', '二班')
List<Expression> list = loginUser.getClassNameList()
.stream()
.map(StringValue::new)
.collect(Collectors.toList());
Column column = new Column(tableAlias == null
? this.columnId
: tableAlias.getName() + "." + this.columnId);
ParenthesedExpressionList expressionList = new ParenthesedExpressionList();
expressionList.withExpressions(list);
return new InExpression(column, expressionList);
}
}
用户维度
@Component
public class StudentDataScope implements DataScope {
public static final String RESOURCE_NAME = "student";
// 使用正则匹配表名
private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^h2student*$");
@Override
public String getResource() {
return RESOURCE_NAME;
}
@Override
public boolean includes(String tableName) {
return TABLE_NAME_PATTERN.matcher(tableName).matches();
}
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
LoginUser loginUser = LoginUserHolder.get();
// 未登录时 1=2 永不满足
if (loginUser == null) {
return new EqualsTo(new LongValue(1), new LongValue(2));
}
// 老师不做学生维度的过滤,由学校/班级维度控制
if (loginUser.isTeacher()) {
return null;
}
// 学生只能查看自己的数据:WHERE id = ?
Column column = new Column(tableAlias == null
? "id"
: tableAlias.getName() + "." + "id");
return new EqualsTo(column, new LongValue(loginUser.getId()));
}
}
自定义 DataPermissionHandler
默认的 DefaultDataPermissionHandler 提供了基础的过滤功能。如果您需要更复杂的控制逻辑(如管理员直接放行),可以实现自己的 DataPermissionHandler:
@Component
public class CustomDataPermissionHandler implements DataPermissionHandler {
private final List<DataScope> dataScopes;
public CustomDataPermissionHandler(List<DataScope> dataScopes) {
this.dataScopes = dataScopes;
}
@Override
public List<DataScope> dataScopes() {
return this.dataScopes;
}
@Override
public List<DataScope> filterDataScopes(String mappedStatementId) {
// 从栈中获取当前 DataPermissionRule
DataPermissionRule rule = DataPermissionRuleHolder.peek();
// 管理员直接放行所有数据
if (LoginUserHolder.isAdmin()) {
return new ArrayList<>();
}
// 根据规则过滤 DataScope
return filterDataScopes(rule);
}
@Override
public boolean ignorePermissionControl(List<DataScope> dataScopeList,
String mappedStatementId) {
// 如果 mappedStatementId 在缓存中,跳过处理
return MappedStatementIdsWithoutDataScope.onAllWithoutSet(
dataScopeList, mappedStatementId);
}
}
| 自定义 DataPermissionHandler 注册为 Spring Bean 后,框架的自动配置会优先使用您的实现。 |
性能优化
框架内置了 MappedStatementIdsWithoutDataScope 缓存机制以提升性能:
-
每次 SQL 解析后,如果当前 mappedStatementId 对应的 SQL 中没有任何表被任何 DataScope 匹配,则该 mappedStatementId 会被记录到缓存中
-
后续该 mappedStatementId 再次执行时,ignorePermissionControl() 直接返回 rue,跳过 SQL 解析,避免不必要的性能开销
-
缓存基于 ConcurrentHashMap 实现,线程安全
// 核心逻辑 (DataPermissionInterceptor 中)
Integer matchNum = DataScopeMatchNumHolder.pollMatchNum();
List<DataScope> allDataScopes = this.dataPermissionHandler.dataScopes();
if (allDataScopes.size() == filterDataScopes.size() && matchNum != null && matchNum == 0) {
// 没有任何匹配,记录到缓存中,下次直接跳过
MappedStatementIdsWithoutDataScope.addToWithoutSet(filterDataScopes, mappedStatementId);
}
| 该优化对字典表、常量表等不需要数据权限控制的 Mapper 特别有效,首次执行后便不再解析。 |
注解使用
将 DataScope 注册为 Bean 后,在 Mapper 上使用 @DataPermission 进行配置:
// 类级别:控制该 Mapper 的所有方法
@DataPermission(includeResources = "order")
@Mapper
public interface OrderMapper {
List<Order> listOrder();
// 方法级别覆盖:排除 order 资源,使用其他 DataScope
@DataPermission(excludeResources = "order")
List<Order> listAllOrder();
}
// 忽略数据权限控制
@DataPermission(ignore = true)
@Mapper
public interface DictMapper {
List<Dict> listDict();
}
编程式控制
推荐在 Service 层使用编程式 API 进行细粒度的权限控制:
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
// 使用注解在方法上控制
@DataPermission(includeResources = ClassDataScope.RESOURCE_NAME)
public List<Student> listStudentOnlyFilterClass() {
return studentMapper.listStudent();
}
// 编程式控制
public List<Student> listStudentWithSchoolOnly() {
DataPermissionRule rule = new DataPermissionRule()
.setIncludeResources(new String[] { SchoolDataScope.RESOURCE_NAME });
return DataPermissionUtils.executeWithDataPermissionRule(rule,
() -> studentMapper.listStudent());
}
// 忽略所有数据权限
public List<Student> listAllStudent() {
return DataPermissionUtils.executeAndIgnoreAll(
() -> studentMapper.listStudent());
}
}
规则优先级
当注解和编程式同时存在时,规则优先级由近及远:
-
方法内部的编程式规则(优先级最高)
-
被调用方法上的 @DataPermission 注解
-
调用者的编程式/注解规则(通过栈式嵌套传递)
-
全局规则(所有 DataScope 参与控制,优先级最低)
// 外层:全局规则(所有DataScope生效)
List<Student> all = studentMapper.listStudent();
// 编程式规则覆盖
DataPermissionUtils.executeWithDataPermissionRule(
new DataPermissionRule().setIncludeResources("class"),
() -> {
// 内层:使用 class 维度的 DataScope
List<Student> byClass = studentMapper.listStudent();
// 嵌套规则覆盖
DataPermissionUtils.executeAndIgnoreAll(() -> {
// 最内层:忽略全部数据权限
List<Student> all2 = studentMapper.listStudent();
});
}
);