实战:基于 MyBatis-Plus 实现无感知的“数据权限”自动过滤
1. 前言
在多租户或多部门系统中,我们希望开发者在写 SQL(如 SELECT * FROM sys_user)时,不需要手动拼接 WHERE tenant_id = 1 或 WHERE dept_id = 100。
本文将基于 MyBatis-Plus 的拦截器机制,结合 JSqlParser 和 Spring AOP,实现一套灵活的、支持注解控制的数据权限框架。
2. 核心架构设计
整个方案可以分为三个层级:
- 策略层 (Rule):定义具体的过滤规则(如部门规则、租户规则)。
- 执行层 (Handler & Factory):负责在 SQL 执行时,将规则拼接进 SQL。
- 控制层 (Annotation & AOP):负责通过注解动态开启、关闭或过滤规则。
核心交互时序图:
3. 核心代码实现
第一步:定义规则接口 (Strategy Pattern)
我们需要一个接口来统一所有的数据权限规则。
public interface DataPermissionRule {
/**
* 获取该规则生效的表名集合
* 作用:快速过滤,只有匹配的表才需要处理,提升性能
*/
Set<String> getTableNames();
/**
* 根据表名和别名,生成 SQL 条件表达式
* @param tableName 表名 (e.g., sys_user)
* @param tableAlias 别名 (e.g., u)
* @return JSqlParser 表达式 (e.g., u.dept_id = 1)
*/
Expression getExpression(String tableName, Alias tableAlias);
}
第二步:实现具体的业务规则 (Rule Implementation)
以“部门数据权限”为例,限制用户只能查询本部门的数据。
@AllArgsConstructor
public class DeptDataPermissionRule implements DataPermissionRule {
private final PermissionApi permissionApi; // 业务 Service,用于获取当前用户的部门权限
@Override
public Set<String> getTableNames() {
// 配置哪些表需要进行部门隔离
return Set.of("sys_user", "sys_dept", "t_order");
}
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
// 1. 获取当前登录用户
Long userId = SecurityUtils.getLoginUserId();
if (userId == null) return null; // 未登录不处理
// 2. 获取用户的部门数据权限 (通常建议做一层 RequestScope 缓存)
Set<Long> deptIds = permissionApi.getUserDeptDataPermission(userId);
if (CollUtil.isEmpty(deptIds)) {
// 如果没有部门权限,直接返回 null = null,保证查不到数据
return new EqualsTo(new NullValue(), new NullValue());
}
// 3. 构造 SQL:dept_id IN (1, 2, 3)
// 注意:这里需要根据别名处理列名,如 u.dept_id
String columnName = MyBatisUtils.getAliasColumn(tableAlias, "dept_id");
return new InExpression(
new Column(columnName),
new ParenthesedExpressionList(new ExpressionList(
deptIds.stream().map(LongValue::new).collect(Collectors.toList())
))
);
}
}
第三步:上下文与 AOP 控制 (Context Holder)
为了让业务层能通过注解灵活控制权限(如 @DataPermission(enable=false)),我们需要 ThreadLocal 上下文。
3.1 上下文持有者
public class DataPermissionContextHolder {
// 使用 LinkedList 支持方法的嵌套调用(入栈/出栈)
private static final ThreadLocal<LinkedList<DataPermission>> CONTEXT =
ThreadLocal.withInitial(LinkedList::new);
public static void add(DataPermission annotation) {
CONTEXT.get().addLast(annotation);
}
public static DataPermission remove() {
LinkedList<DataPermission> list = CONTEXT.get();
DataPermission last = list.removeLast();
if (list.isEmpty()) CONTEXT.remove(); // 清理 ThreadLocal,防内存泄露
return last;
}
public static DataPermission get() {
return CONTEXT.get().peekLast(); // 获取栈顶(当前生效)的配置
}
}
3.2 AOP 拦截器
@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
public class DataPermissionAnnotationInterceptor implements MethodInterceptor {
/**
* DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位
*/
static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class);
@Getter
private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>();
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
// 入栈
DataPermission dataPermission = this.findAnnotation(methodInvocation);
if (dataPermission != null) {
DataPermissionContextHolder.add(dataPermission);
}
try {
// 执行逻辑
return methodInvocation.proceed();
} finally {
// 出栈
if (dataPermission != null) {
DataPermissionContextHolder.remove();
}
}
}
private DataPermission findAnnotation(MethodInvocation methodInvocation) {
// 1. 从缓存中获取
Method method = methodInvocation.getMethod();
Object targetObject = methodInvocation.getThis();
Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass();
MethodClassKey methodClassKey = new MethodClassKey(method, clazz);
DataPermission dataPermission = dataPermissionCache.get(methodClassKey);
if (dataPermission != null) {
return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null;
}
// 2.1 从方法中获取
dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class);
// 2.2 从类上获取
if (dataPermission == null) {
dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class);
}
// 2.3 添加到缓存中
dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL);
return dataPermission;
}
}
3.3 切面 Advisor
/**
* DataPermission 注解的 Advisor
* 职责:将拦截器 (Interceptor) 与切点 (Pointcut) 绑定
*/
@Getter
@EqualsAndHashCode(callSuper = true)
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor {
private final Advice advice;
private final Pointcut pointcut;
/**
* 构造函数
* @param interceptor 我们在 3.2 节定义的拦截器
*/
public DataPermissionAnnotationAdvisor(DataPermissionAnnotationInterceptor interceptor) {
this.advice = interceptor;
this.pointcut = this.buildPointcut();
}
/**
* 构建切点逻辑 (核心匹配规则)
*/
protected Pointcut buildPointcut() {
// 1. 类级别的匹配器:检查类头上是否有 @DataPermission
// 第二个参数 true 表示 checkInherited,支持继承的注解
Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true);
// 2. 方法级别的匹配器:检查方法头上是否有 @DataPermission
// 第一个参数 null 表示不关心类注解,只关心方法注解
Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true);
// 3. 组合逻辑 (Union)
// 含义:(类上有注解) OR (方法上有注解) -> 都算命中
// 使用 Spring 的 ComposablePointcut 进行组合
return new ComposablePointcut(classPointcut).union(methodPointcut);
}
}
第四步:规则决策工厂 (Factory)
工厂负责根据当前的 AOP 上下文,筛选出本次 SQL 执行需要哪些规则。
@RequiredArgsConstructor
public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory {
private final List<DataPermissionRule> rules; // Spring 注入所有 Rule Bean
@Override
public List<DataPermissionRule> getDataPermissionRules() {
// 1. 获取上下文中的注解配置
DataPermission activeAnnotation = DataPermissionContextHolder.get();
// 2. 如果没注解,默认返回所有规则(安全兜底)
if (activeAnnotation == null) {
return rules;
}
// 3. 如果显式禁用,返回空
if (!activeAnnotation.enable()) {
return Collections.emptyList();
}
// 4. 处理 Include(白名单)和 Exclude(黑名单)
// 这里使用 Stream 过滤,逻辑:Include 优先于 Exclude
return rules.stream().filter(rule -> {
Class<?> ruleClass = rule.getClass();
if (ArrayUtil.isNotEmpty(activeAnnotation.includeRules())) {
return ArrayUtil.contains(activeAnnotation.includeRules(), ruleClass);
}
if (ArrayUtil.isNotEmpty(activeAnnotation.excludeRules())) {
return !ArrayUtil.contains(activeAnnotation.excludeRules(), ruleClass);
}
return true;
}).collect(Collectors.toList());
}
}
第五步:MyBatis-Plus 处理器 (Handler)
这是连接业务逻辑与 MyBatis 底层的桥梁。它实现 MP 的 MultiDataPermissionHandler 接口。
@RequiredArgsConstructor
public class DataPermissionRuleHandler implements MultiDataPermissionHandler {
private final DataPermissionRuleFactory ruleFactory;
@Override
public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) {
// 1. 从工厂拿到当前生效的规则列表
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRules();
if (CollUtil.isEmpty(rules)) return null;
Expression allExpression = null;
// 2. 遍历所有规则,拼装 AND 条件
for (DataPermissionRule rule : rules) {
// 2.1 只要管辖范围内的表
if (!rule.getTableNames().contains(table.getName())) {
continue;
}
// 2.2 生成单条条件 (e.g., dept_id = 1)
Expression oneExpress = rule.getExpression(table.getName(), table.getAlias());
if (oneExpress == null) continue;
// 2.3 拼接到总条件中:WHERE (old) AND (rule1) AND (rule2)
allExpression = allExpression == null ? oneExpress
: new AndExpression(allExpression, oneExpress);
}
return allExpression;
}
}
第六步:最终组装与配置 (Configuration)
将上述组件组装起来,并注入到 MyBatis-Plus 的拦截器链中。
@Configuration
public class DataPermissionConfiguration {
@Bean
public DataPermissionRule deptDataPermissionRule(PermissionApi permissionApi) {
DataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
return rule;
}
@Bean
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) {
return new DataPermissionRuleFactoryImpl(rules);
}
@Bean
public DataPermissionRuleHandler dataPermissionRuleHandler(DataPermissionRuleFactory ruleFactory) {
return new DataPermissionRuleHandler(ruleFactory);
}
/**
* 配置 MP 的拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(DataPermissionRuleHandler handler) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 创建数据权限拦截器
DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor();
// 2. 【关键】dataPermissionInterceptor没有使用自动注入,需要手动注入 Handler
dataPermissionInterceptor.setDataPermissionHandler(handler);
// 3. 添加到拦截器链
interceptor.addInnerInterceptor(dataPermissionInterceptor);
// (可选) 添加分页拦截器等...
return interceptor;
}
}
4. 实战指南:如何在业务开发中使用?
框架搭建完毕后,在日常业务开发中,数据权限的使用主要分为三种场景:默认自动生效、注解灵活控制、编程式临时控制。
4.1 场景一:默认无感知过滤 (The Magic)
这是最常见的场景。开发者在编写 Service 代码时,完全不需要关心权限逻辑,就像写普通的 CRUD 一样。
业务代码:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
public List<UserDO> getUserList() {
// 开发者只写了查询所有,没加任何 WHERE 条件
return userMapper.selectList(null);
}
}
实际执行的 SQL:
框架会自动识别当前登录人(如租户 ID=1,部门 ID=100),并改写 SQL:
SELECT * FROM sys_user
WHERE tenant_id = 1 -- 自动拼接租户规则
AND dept_id = 100 -- 自动拼接部门规则
注意:前提是
sys_user表必须包含在DeptDataPermissionRule的getTableNames()返回集合中。
4.2 场景二:基于注解的策略控制 (Annotation)
当我们需要打破默认规则时(例如:管理员查询、跨部门统计、后台定时任务),使用 @DataPermission 注解是最优雅的方式。
2.1 彻底关闭权限(上帝视角)
适用于后台管理统计、数据迁移、定时任务等场景。
@DataPermission(enable = false) // 关掉所有规则
public List<UserDO> getAllUsersForAdmin() {
// 执行 SQL: SELECT * FROM sys_user
// 结果:查出全库所有租户、所有部门的数据
return userMapper.selectList(null);
}
2.2 排除特定规则(黑名单)
适用于“虽然我是 A 租户的人,但我要查询所有租户的信息,不过我只能看我自己部门的”这种复杂场景。
// 排除租户规则,但保留部门规则(以及其他规则)
@DataPermission(excludeRules = TenantDataPermissionRule.class)
public List<UserDO> getMultiTenantUsers() {
// 执行 SQL: SELECT * FROM sys_user WHERE dept_id = 100
// 结果:租户限制消失了,但部门限制还在
return userMapper.selectList(null);
}
2.3 只启用特定规则(白名单)
适用于非常严格的场景,确保只有核心规则生效,防止其他规则(如刚开发的实验性规则)干扰。
// 只启用部门规则,忽略其他一切
@DataPermission(includeRules = DeptDataPermissionRule.class)
public void onlyDeptLogic() { ... }
4.3 场景三:代码块级临时控制 (Utils)
如果你不能修改 Service 方法的签名(比如是继承的父类方法),或者你只想在方法内部的某一行查询暂时忽略权限,注解就不好用了。这时我们需要一个工具类 DataPermissionUtils。
工具类实现 (Util Implementation):
public class DataPermissionUtils {
public static <T> T executeIgnore(Supplier<T> supplier) {
// 1. 压入一个“禁用”配置
DataPermissionContextHolder.add(new DataPermission() {
public boolean enable() { return false; }
// ... 其他默认实现
});
try {
// 2. 执行业务逻辑
return supplier.get();
} finally {
// 3. 恢复现场
DataPermissionContextHolder.remove();
}
}
}
业务使用 (Usage):
public void complexBusiness() {
// 1. 这里的查询受权限控制
userMapper.selectList(null);
// 2. 这里的查询临时忽略权限
List<UserDO> allData = DataPermissionUtils.executeIgnore(() -> {
return userMapper.selectList(null);
});
// 3. 这里的查询恢复受权限控制
userMapper.selectById(1);
}
4.4 前置条件:上下文的填充 (Important!)
这就回到了我们定义的 Rule 实现类。所有的过滤逻辑都依赖于 “当前登录人是谁”。
在 Web 环境中,通常通过 Filter 或 Interceptor 在请求开始时,将用户信息放入 ThreadLocal(如 Spring Security 的 SecurityContextHolder)。
如果是在单元测试或非 Web 环境中,使用 Service 前必须先模拟登录,否则 Rule 获取不到 UserID 可能会抛出异常或返回空结果。
单元测试示例:
@Test
public void testDataPermission() {
// 1. Mock 登录用户 (放入 Security 上下文)
SecurityUtils.mockLoginUser(new LoginUser().setId(1L).setDeptId(100L));
// 2. 执行 Service,此时 Rule 才能拿到 dept_id = 100
List<UserDO> users = userService.getUserList();
// 3. 断言 SQL 是否拼接正确
Assert.assertTrue(users.size() > 0);
}
5. 进阶篇:基于 Customizer 的规则动态扩展
在上一章节的实现中,我们在 DeptDataPermissionRule 内部写死了 Set.of("sys_user", "sys_dept")。在简单的单体项目中这没问题,但在多模块的大型系统中,这违背了 “开闭原则”:
- 痛点:每次新增业务模块(如
order-module),都需要修改底层的DeptDataPermissionRule代码来添加新表。 - 目标:底层框架只提供“过滤能力”,具体的“过滤名单”交由各业务模块自己注册。
我们将采用 Customizer(自定义器)模式 对架构进行重构。
5.1 定义自定义器接口
首先,定义一个回调接口,允许业务模块获取到 Rule 对象,并往里面添加配置。
@FunctionalInterface
public interface DataPermissionRuleCustomizer<T extends DataPermissionRule> {
void customize(T rule);
}
5.2 重构抽象规则类 (Rule)
抽象类AbstractDataPermissionRule提供建立Expression的基本工具方法,被具体的rule类继承
public abstract class AbstractDataPermissionRule implements DataPermissionRule {
// 存储配置:Key=表名, Value=规则列表 (一张表可能有多个字段控制,如同时有部门字段和密级字段)
private final Map<String, List<RuleConfig>> ruleConfigs = new HashMap<>();
// --- 1. 配置注册区 (供 Customizer 调用) ---
public void addRule(Class<?> entityClass, String columnName, ConditionTypeEnum type) {
String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
ruleConfigs.computeIfAbsent(tableName, k -> new ArrayList<>())
.add(new RuleConfig(columnName, type));
}
// 快捷方法:注册 IN
public void addInColumn(Class<?> entityClass, String columnName) {
addRule(entityClass, columnName, ConditionTypeEnum.IN);
}
// 快捷方法:注册 EQUALS
public void addEqualColumn(Class<?> entityClass, String columnName) {
addRule(entityClass, columnName, ConditionTypeEnum.EQUALS);
}
// 快捷方法:注册右模糊 (用于部门树 dept_tree_path like '0,100,%')
public void addLikeRightColumn(Class<?> entityClass, String columnName) {
addRule(entityClass, columnName, ConditionTypeEnum.LIKE_RIGHT);
}
@Override
public Set<String> getTableNames() {
return ruleConfigs.keySet();
}
// --- 2. 核心工具区 (供子类调用) ---
/**
* 获取某张表下,指定类型的字段配置
*/
protected String getColumn(String tableName, ConditionTypeEnum type) {
List<RuleConfig> configs = ruleConfigs.get(tableName);
if (configs == null) return null;
return configs.stream()
.filter(c -> c.getType() == type)
.map(RuleConfig::getColumnName)
.findFirst().orElse(null);
}
/**
* 【万能构建器】根据类型构建 JSqlParser 表达式
* @param alias 表别名
* @param columnName 字段名
* @param type 操作类型
* @param value 业务数据 (可以是单个值,也可以是集合)
*/
protected Expression buildExpression(String tableName, Alias alias, String columnName, ConditionTypeEnum type, Object value) {
if (StrUtil.isEmpty(columnName)) return null;
if (value == null && type != ConditionTypeEnum.IS_NULL && type != ConditionTypeEnum.IS_NOT_NULL) return null;
// 1. 处理列名 (e.g. u.dept_id)
String finalColumn = MyBatisUtils.getGuardedColumn(tableName, alias, columnName);
Column columnObj = new Column(finalColumn);
// 2. 根据类型生成表达式
switch (type) {
case EQUALS: return new EqualsTo(columnObj, new LongValue(value.toString()));
case NOT_EQUALS: return new NotEqualsTo(columnObj, new LongValue(value.toString()));
case IN:
Collection<?> coll = (Collection<?>) value;
if (CollUtil.isEmpty(coll)) return null; // 或者返回 1!=1
ExpressionList items = new ExpressionList(
coll.stream().map(v -> new LongValue(v.toString())).collect(Collectors.toList())
);
return new InExpression(columnObj, new ParenthesedExpressionList(items));
case LIKE_RIGHT: // LIKE 'abc%'
return new LikeExpression()
.withLeftExpression(columnObj)
.withRightExpression(new StringValue(value.toString() + "%"));
case GT: return buildBinary(new GreaterThan(), columnObj, value);
case GE: return buildBinary(new GreaterThanEquals(), columnObj, value);
case LT: return buildBinary(new MinorThan(), columnObj, value);
case LE: return buildBinary(new MinorThanEquals(), columnObj, value);
case IS_NULL: return new IsNullExpression().withLeftExpression(columnObj);
default: return null;
}
}
private BinaryExpression buildBinary(BinaryExpression expr, Column column, Object value) {
expr.setLeftExpression(column);
expr.setRightExpression(new LongValue(value.toString()));
return expr;
}
// 简单的配置内部类
@Data
@AllArgsConstructor
private static class RuleConfig {
private String columnName;
private ConditionTypeEnum type;
}
}
5.3 重构具体规则类
DeptDataPermissionRule 主要重写关键的getExpression方法,该方法与具体业务相关,因此不在抽象类中,其后续被DataPermissionRuleHandler.getSqlSegment()调用,用来添加条件SQL语句。
注意将当前用户的deptDataPermission放到上下文中,因为该用户登录后,后续请求大多都会经过getExpression来获取条件表达式,如果不进行缓存,每次都要去数据库查询,十分耗时。
@AllArgsConstructor
@Slf4j
public class DeptDataPermissionRule extends AbstractDataPermissionRule {
private final PermissionCommonApi permissionApi;
protected static final String CONTEXT_KEY = DeptDataPermissionRule .class.getSimpleName();
static final Expression EXPRESSION_NULL = new NullValue();
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
// 只有有登陆用户的情况下,才进行数据权限的处理
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser == null) {
return null;
}
// 只有管理员类型的用户,才进行数据权限的处理
if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
return null;
}
// 获得数据权限
DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
// 从上下文中拿不到,则调用逻辑进行获取
if (deptDataPermission == null) {
deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
if (deptDataPermission == null) {
log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
loginUser.getId(), tableName, tableAlias.getName()));
}
// 添加到上下文中,避免重复计算
loginUser.setContext(CONTEXT_KEY, deptDataPermission);
}
// 情况一,如果是 ALL 可查看全部,则无需拼接条件
if (deptDataPermission.getAll()) {
return null;
}
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
}
String inColumnName = getColumn(tableName, ConditionTypeEnum.IN);
String equalColumnName = getColumn(tableName, ConditionTypeEnum.EQUALS);
// 情况三,拼接 Dept 和 User 的条件,最后组合
Expression inExpression = buildExpression(tableName, tableAlias, inColumnName, ConditionTypeEnum.IN, deptDataPermission.getDeptIds());
Expression equalExpression;
if(deptDataPermission.getSelf())
equalExpression = buildExpression(tableName, tableAlias, equalColumnName, ConditionTypeEnum.EQUALS, loginUser.getId());
else
equalExpression = null;
if (inExpression == null && equalExpression == null) {
// TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
// loginUser.getId(), tableName, tableAlias.getName()));
return EXPRESSION_NULL;
}
if (inExpression == null) {
return equalExpression;
}
if (equalExpression == null) {
return inExpression;
}
// 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
return new ParenthesedExpressionList(new OrExpression(inExpression, equalExpression));
}
}
5.4 业务模块的配置 (Usage)
现在,如果 system 模块想让 sys_user 表支持数据权限,首先需要在 system 模块下写一个配置类注册Custormizer:
@Configuration(proxyBeanMethods = false)
public class SystemDataPermissionConfiguration {
@Bean
public DataPermissionRuleCustomizer<MyDeptDataPermissionRule> sysMyDeptDataPermissionRuleCustomizer() {
return rule -> {
//当SQL操作system_users表时,插入In语句system_users.dept_id in (...)
rule.addInColumn(AdminUserDO.class, "dept_id");
//当SQL操作system_depts表时,插入In语句system_depts.id in (...)
rule.addInColumn(DeptDO.class, "id");
//当SQL操作system_users表时,插入Equals语句system_users.id = ...
rule.addEqualColumn(AdminUserDO.class, "id");
};
}
}
5.5 自动化装配与 Bean 注册 (AutoConfiguration)
最后,我们需要一个自动配置类,将 Customizer 收集起来,注入到 DeptDataPermissionRule 中,并将 Rule 注册为 Spring Bean。
@AutoConfiguration
@ConditionalOnClass(LoginUser.class)
@ConditionalOnBean(value = {DataPermissionRuleCustomizer.class})
public class YudaoDeptDataPermissionAutoConfiguration {
@Bean
public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionCommonApi,
List<DataPermissionRuleCustomizer<DeptDataPermissionRule>> customizers) {
// 创建 MyDeptDataPermissionRule 对象
DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionCommonApi);
// 补全表配置
customizers.forEach(customizer -> customizer.customize(rule));
return rule;
}
}
6. 扩展:新增数据权限规则并应用(以租户隔离为例)
随着业务发展,单一的部门数据权限可能无法满足所有需求。例如,在 SaaS 场景下,我们还需要实现租户隔离:不同租户的数据必须严格隔离。
6.1 扩展思路
要新增一种数据权限规则(如租户规则),我们需要完成以下四步闭环,它们紧密围绕 Spring 的 IoC 和 Customizer 模式:
- 定义新的规则类 (Rule):继承
AbstractDataPermissionRule类,并实现getExpression方法,包含新规则的业务逻辑。 - 定义对应的 Customizer 类:允许业务模块注册表名到新规则中。
- 注册Customizer为Bean
- 注册新的 Rule Bean:通过
@Configuration和@Bean将新规则注册到 Spring 容器,并注入所有对应的 Customizer 进行配置。
6.2 第一步:定义新的规则类 (TenantDataPermissionRule)
新建一个类 TenantDataPermissionRule,它将负责租户 ID 的获取和 SQL 片段的生成。
// TenantDataPermissionRule.java
@RequiredArgsConstructor // Lombok 注解,自动生成构造函数
public class TenantDataPermissionRule extends AbstractDataPermissionRule {
private final PermissionCommonApi permissionApi;
protected static final String CONTEXT_KEY = TenantDataPermissionRule .class.getSimpleName();
static final Expression EXPRESSION_NULL = new NullValue();
@Override
public Expression getExpression(String tableName, Alias tableAlias) {
// 只有有登陆用户的情况下,才进行数据权限的处理
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser == null) {
return null;
}
// 只有管理员类型的用户,才进行数据权限的处理
if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
return null;
}
// 获得数据权限
TenantDataPermissionRespDTO tenantDataPermission = loginUser.getContext(CONTEXT_KEY, TenantDataPermissionRespDTO.class);
// 从上下文中拿不到,则调用逻辑进行获取
if (tenantDataPermission == null) {
tenantDataPermission = permissionApi.getTenantDataPermission(loginUser.getId());
if (tenantDataPermission == null) {
log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
loginUser.getId(), tableName, tableAlias.getName()));
}
// 添加到上下文中,避免重复计算
loginUser.setContext(CONTEXT_KEY, tenantDataPermission);
}
// 情况一,如果是 ALL 可查看全部,则无需拼接条件
if (tenantDataPermission.getAll()) {
return null;
}
// 情况二,即不能查看租户,又不能查看自己,则说明 100% 无权限
if (CollUtil.isEmpty(tenantDataPermission.getTenantIds())
&& Boolean.FALSE.equals(tenantDataPermission.getSelf())) {
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
}
String inColumnName = getColumn(tableName, ConditionTypeEnum.IN);
String equalColumnName = getColumn(tableName, ConditionTypeEnum.EQUALS);
// 情况三,拼接 Dept 和 User 的条件,最后组合
Expression inExpression = buildExpression(tableName, tableAlias, inColumnName, ConditionTypeEnum.IN, deptDataPermission.getTenantIds());
Expression equalExpression;
if(tenantDataPermission.getSelf())
equalExpression = buildExpression(tableName, tableAlias, equalColumnName, ConditionTypeEnum.EQUALS, loginUser.getId());
else
equalExpression = null;
if (inExpression == null && equalExpression == null) {
// TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
log.warn("[getExpression][LoginUser({}) Table({}/{}) TenantDataPermission({}) 构建的条件为空]",
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(tenantDataPermission));
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
// loginUser.getId(), tableName, tableAlias.getName()));
return EXPRESSION_NULL;
}
if (inExpression == null) {
return equalExpression;
}
if (equalExpression == null) {
return inExpression;
}
// 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?)
return new ParenthesedExpressionList(new OrExpression(inExpression, equalExpression));
}
}
6.3 第二步:定义对应的 Customizer 类并注册
@Bean
public DataPermissionRuleCustomizer<TenantDeptDataPermissionRule> sysTenantDeptDataPermissionRuleCustomizer() {
return rule -> {
rule.addInColumn(AdminUserDO.class, "tenant_id");
rule.addInColumn(TenantDO.class, "id");
rule.addEqualColumn(AdminUserDO.class, "id");
};
}
6.4 第三步:将新 Rule 注册为 Spring Bean 并注入 Customizer
现在,我们需要一个新的 @Configuration 类(或者在原来的AutoConfiguration类中),负责创建 TenantDataPermissionRule 的实例,并将所有 TenantDataPermissionRuleCustomizer 应用到它上面。
@Configuration
@ConditialOnBean(value={DataPermissionRuleCustomizer.class})
public class YudaoTenantDataPermissionAutoConfiguration {
// 1. 注册 TenantDataPermissionRule Bean
@Bean
public TenantDataPermissionRule tenantDataPermissionRule(
// 2. Spring 自动收集所有实现了 TenantDataPermissionRuleCustomizer 的 Bean
List<DataPermissionRuleCustomizer<TenantDataPermissionRule>> customizers) {
// 3. 创建 TenantDataPermissionRule 实例 (初始是空的)
TenantDataPermissionRule rule = new TenantDataPermissionRule();
// 4. 【核心】遍历所有 Customizer,应用它们的定制化配置
customizers.forEach(customizer -> customizer.customize(rule));
return rule;
}
}
6.5 自动融合:新规则如何生效?
无需修改 DataPermissionRuleFactoryImpl 和 DataPermissionRuleHandler!
- Factory 自动收集:
DataPermissionRuleFactoryImpl的构造函数注入的是List<DataPermissionRule>。因为它是一个泛型列表,Spring 会自动将你新注册的TenantDataPermissionRuleBean,连同之前的DeptDataPermissionRuleBean,一起注入到这个列表中。 - Handler 自动执行:
DataPermissionRuleHandler的getSqlSegment方法会遍历从 Factory 拿到的所有DataPermissionRule。当它遇到TenantDataPermissionRule时,就会执行其getTableNames()和getExpression()方法,自动拼接上租户的WHERE条件。
Q&A
Advisor的代码不是说,如果类和方法上没有写@DataPermission就不会被DataPermissionAnnotationInterceptor拦截么,没有被DataPermissionAnnotationInterceptor拦截就不会有dataPermission上下文,那么就不会加where
这是一个基于默认安全策略的设计:注解的作用是“申请特权”,而不是“开启防护”。
DataPermissionAnnotationAdvisor 确实只拦截带 @DataPermission 的方法,因此对于普通的业务方法,拦截器不会执行,DataPermissionContextHolder.get() 拿到的结果就是 null。但这恰恰触发了 DataPermissionRuleFactoryImpl 的默认兜底逻辑:它一旦检测到上下文为 null,就会判定为“当前无特殊指示”,从而强制返回所有配置的规则(return rules)。这就确保了开发者在写代码时,什么都不用做就是最安全的(自动拼接所有 WHERE 条件);只有当需要打破规则(如管理员查询 enable=false)时,才需要加注解来生成上下文,指示工厂去“放行”。
核心代码证据如下:
// DataPermissionRuleFactoryImpl.java
@Override
public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) {
// 1. 对于没加 @DataPermission 的方法,拦截器不执行,这里拿到 null
DataPermission dataPermission = DataPermissionContextHolder.get();
// 2. 【关键逻辑】Context 为 null 并非“不处理”,而是“全量处理”
if (dataPermission == null) {
return rules; // 返回容器中所有的规则(如租户规则、部门规则等)
}
// 3. 只有当 dataPermission != null (有注解) 时,才根据注解去排除或禁用规则
if (!dataPermission.enable()) {
return Collections.emptyList();
}
// ... Include/Exclude 处理
}
DataPermissionRuleHandler的getSqlSegment方法又是被谁调用的呢?为什么只需要重写这个方法就能实现无感添加Where的功能。
getSqlSegment 方法是由 MyBatis-Plus 的内部拦截器 DataPermissionInterceptor**(继承自 BaseMultiTableInnerInterceptor)在 SQL 执行前的拦截阶段调用的。之所以只需重写该方法就能实现无感添加 WHERE 条件,是因为 MyBatis-Plus 利用 JSqlParser 将原始 SQL 解析为抽象语法树(AST),拦截器会遍历这棵树中的所有表,调用你的 getSqlSegment 获取额外的权限条件片段(如 dept_id = 1),然后通过动态改写 AST**,将这些条件以 AND 的方式强制追加到原 SQL 的 WHERE 子句中,最终执行的是改写后的 SQL,从而实现了业务代码无感知的底层数据隔离。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)