1. 数据权限的痛点与无侵入方案的价值
在企业级应用开发中,数据权限控制是个永恒的话题。我经历过太多项目,看到开发团队为了实现数据权限绞尽脑汁——有的在业务代码里硬编码SQL条件,有的在DAO层做各种if-else判断,最糟糕的是在Controller里写满权限过滤逻辑。这些做法不仅让代码变得臃肿,更可怕的是每次权限规则变更都需要全量回归测试。
数据权限的本质是:基于当前用户身份动态过滤数据结果集。传统实现方式有三大弊端:
- 代码侵入性强:权限逻辑与业务代码深度耦合,违反了单一职责原则
- 维护成本高:权限规则散落在各处,修改时容易遗漏
- 性能隐患:多次查询后再过滤,浪费数据库资源
MyBatis拦截器提供了一种优雅的解决方案。通过拦截Executor的query方法,我们可以在SQL执行前动态修改语句,实现"一次编写,全局生效"的数据权限控制。这种方案的优势在于:
- 零侵入:业务代码无需任何修改
- 集中管理:所有权限规则在一个地方维护
- 执行高效:过滤在数据库层面完成
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MyBatis拦截器机制深度解析
2.1 拦截器的工作原理
MyBatis采用责任链模式处理拦截器,核心接口Interceptor定义了三个关键方法:
java复制public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
拦截器生效流程如下:
- 通过@Intercepts注解声明要拦截的方法
- MyBatis启动时通过plugin()方法生成代理对象
- 执行时调用intercept()方法进入拦截逻辑
对于数据权限场景,我们需要重点关注Executor的query方法:
java复制@Intercepts({
@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
2.2 SQL重写关键技术点
在拦截器中修改SQL需要处理几个关键问题:
- SQL解析:需要准确识别表名、字段名,避免误改
- 条件拼接:要正确处理原有WHERE条件,处理AND/OR优先级
- 参数处理:动态参数需要与原始参数合并
推荐使用JSqlParser进行SQL解析:
java复制Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Select) {
Select select = (Select) statement;
// 在此处修改Select对象
}
警告:直接字符串拼接SQL存在SQL注入风险,务必使用参数化方式添加条件
3. 完整实现方案与核心代码
3.1 基础拦截器框架搭建
首先创建基础拦截器类:
java复制@Intercepts({
@Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
@Slf4j
public class DataPermissionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 实现细节在下文展开
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可读取配置参数
}
}
3.2 权限条件动态注入
核心拦截逻辑实现:
java复制public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 1. 判断是否需要数据权限过滤
if (!needDataPermission(ms.getId())) {
return invocation.proceed();
}
// 2. 获取当前用户权限规则
String permissionCondition = getPermissionCondition();
if (StringUtils.isEmpty(permissionCondition)) {
return invocation.proceed();
}
// 3. 修改SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String newSql = boundSql.getSql() + " AND " + permissionCondition;
// 4. 重建BoundSql
BoundSql newBoundSql = new BoundSql(...);
// 参数处理省略...
// 5. 修改MappedStatement
MappedStatement newMs = copyMappedStatement(ms, newBoundSql);
args[0] = newMs;
return invocation.proceed();
}
3.3 权限规则管理
建议将权限规则抽象为独立服务:
java复制public interface DataPermissionService {
/**
* 获取当前用户的权限SQL片段
* @return 如 "dept_id IN (1001,1002)"
*/
String getPermissionSql(String mapperId);
}
实现示例:
java复制@Service
public class DeptDataPermissionService implements DataPermissionService {
@Override
public String getPermissionSql(String mapperId) {
User user = SecurityUtils.getCurrentUser();
if (user.isAdmin()) {
return ""; // 管理员不过滤
}
return "dept_id IN (" + user.getAccessDepts() + ")";
}
}
4. 高级应用与性能优化
4.1 多租户场景适配
对于SaaS系统,需要同时处理租户隔离和数据权限:
sql复制-- 最终SQL示例
SELECT * FROM orders
WHERE tenant_id = 123
AND dept_id IN (1001,1002)
AND create_by = 'user1'
建议采用注解方式声明权限维度:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
String[] dimensions() default {};
}
4.2 性能优化方案
- 缓存权限SQL:用户权限通常变化不频繁,可缓存权限条件
- 避免全表扫描:确保权限字段有索引
- 批量查询优化:对于IN条件过大的情况,改用临时表方案
java复制// 使用Guava缓存示例
LoadingCache<String, String> permissionCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> permissionService.getPermissionSql(key));
4.3 与MyBatis-Plus的集成
如果项目使用MyBatis-Plus,需要注意:
- 避免与MP的分页插件冲突,调整拦截器顺序
- 处理Wrapper转SQL时的特殊语法
- 适配MP的自动填充字段
建议配置:
yaml复制mybatis-plus:
configuration:
interceptors:
- com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor
- com.example.DataPermissionInterceptor # 我们的拦截器要放在后面
5. 生产环境踩坑实录
5.1 分页总数查询问题
当使用PageHelper等分页插件时,count查询也会被拦截。需要特殊处理:
java复制if (ms.getId().endsWith("_COUNT")) {
return invocation.proceed();
}
5.2 嵌套查询处理
对于包含子查询的SQL,需要递归处理所有SelectBody:
java复制SelectVisitor adapter = new SelectVisitorAdapter() {
@Override
public void visit(PlainSelect plainSelect) {
// 处理主查询和子查询
}
};
select.getSelectBody().accept(adapter);
5.3 多数据源适配
在动态数据源环境下,需要注意:
- 拦截器会被所有数据源共享
- 需要根据当前数据源决定是否启用权限控制
- 不同数据源可能有不同的权限规则
解决方案:
java复制String currentDs = DynamicDataSourceContextHolder.getDataSourceType();
if ("noAuthDs".equals(currentDs)) {
return invocation.proceed();
}
6. 最佳实践建议
- 白名单机制:配置不需要拦截的Mapper方法
- 开关控制:支持动态启用/禁用权限拦截
- 审计日志:记录SQL修改前后的变化
- 测试策略:
- 单元测试:验证SQL改写正确性
- 集成测试:模拟不同权限用户查询
- 性能测试:对比拦截前后的查询耗时
配置示例:
java复制public class DataPermissionConfig {
@Bean
public DataPermissionInterceptor dataPermissionInterceptor() {
DataPermissionInterceptor interceptor = new DataPermissionInterceptor();
interceptor.setExcludeMappers(Arrays.asList(
"com.example.mapper.ConfigMapper.*",
"com.example.mapper.PublicDataMapper.selectAll"
));
return interceptor;
}
}
在Spring Boot中注册拦截器:
java复制@Configuration
@AutoConfigureAfter(MybatisPlusAutoConfiguration.class)
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
@Bean
public DataPermissionInterceptor dataPermissionInterceptor() {
return new DataPermissionInterceptor();
}
}
经过多个项目的实践验证,这套方案能够满足90%以上的数据权限需求。关键在于将业务规则与技术实现解耦,让专业的人做专业的事——DBA负责优化权限SQL性能,业务专家定义权限规则,开发人员专注于业务逻辑实现。
