全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

数据权限拦截器

数据权限在角色权限之上进一步约束用户可见数据范围,实现"同一角色、不同数据范围"的精细化控制。本文基于 MyBatis 拦截器 + JSqlParser 实现动态 SQL 权限过滤。

数据权限模型

权限类型过滤条件适用场景
全部数据无过滤管理员、CEO
本部门及下级dept_id IN (子部门树)部门经理
本部门dept_id = 当前部门部门主管
本人create_by = 当前用户普通员工
自定义id IN (授权列表)特殊授权场景
XML
权限层级模型
┌────────────────────────────┐
│  全部数据 (ADMIN)           │
│  ┌──────────────────────┐  │
│  │  本部门及下级 (MGR)   │  │
│  │  ┌────────────────┐  │  │
│  │  │  本部门 (LEAD)  │  │  │
│  │  │  ┌──────────┐  │  │  │
│  │  │  │ 本人(EMP)│  │  │  │
│  │  │  └──────────┘  │  │  │
│  │  └────────────────┘  │  │
│  └──────────────────────┘  │
└────────────────────────────┘

JSqlParser SQL 解析

为什么使用 JSqlParser

简单字符串替换无法正确处理以下场景:

  • 子查询中的 WHERE
  • JOIN 关联条件
  • 多表别名
  • UNION/EXISTS 嵌套

JSqlParser 将 SQL 解析为抽象语法树(AST),可在正确的位置注入条件。

核心依赖

Java
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.7</version>
</dependency>

数据权限拦截器实现

权限上下文

Java
public class DataPermissionContext {

    private static final ThreadLocal<DataScope> SCOPE = new ThreadLocal<>();

    public static void setScope(DataScope scope) {
        SCOPE.set(scope);
    }

    public static DataScope getScope() {
        return SCOPE.get();
    }

    public static void clear() {
        SCOPE.remove();
    }

    /** 数据权限范围 */
    public static class DataScope {
        private final String deptId;        // 当前部门
        private final List<String> deptTree; // 部门树(含子部门)
        private final String userId;        // 当前用户
        private final DataScopeType type;   // 权限类型

        public enum DataScopeType {
            ALL,         // 全部数据
            DEPT_TREE,   // 本部门及下级
            DEPT,        // 本部门
            SELF,        // 本人
            CUSTOM       // 自定义
        }
    }
}

拦截器核心

Java
@Intercepts({
    @Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataPermissionInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];

        DataPermissionContext.DataScope scope = DataPermissionContext.getScope();
        if (scope == null || scope.getType() == DataPermissionContext.DataScopeType.ALL) {
            return invocation.proceed();
        }

        // 检查是否忽略数据权限
        if (isIgnorePermission(ms)) {
            return invocation.proceed();
        }

        BoundSql boundSql = ms.getBoundSql(parameter);
        String originalSql = boundSql.getSql();

        // 使用 JSqlParser 解析并注入权限条件
        String newSql = addPermissionCondition(originalSql, scope);

        // 替换 BoundSql
        MappedStatement newMs = copyMappedStatement(ms, newSql, boundSql);
        args[0] = newMs;

        return invocation.proceed();
    }

    private String addPermissionCondition(String sql, DataPermissionContext.DataScope scope) {
        try {
            Statement stmt = CCJSqlParserUtil.parse(sql);

            if (stmt instanceof Select) {
                Select select = (Select) stmt;
                SelectBody selectBody = select.getSelectBody();

                if (selectBody instanceof PlainSelect) {
                    PlainSelect plainSelect = (PlainSelect) selectBody;
                    Expression where = plainSelect.getWhere();
                    Expression permissionExpr = buildPermissionExpression(scope);

                    if (where != null) {
                        AndExpression andExpr = new AndExpression(where, permissionExpr);
                        plainSelect.setWhere(andExpr);
                    } else {
                        plainSelect.setWhere(permissionExpr);
                    }
                }
                return select.toString();
            }
        } catch (JSQLParserException e) {
            // 解析失败时降级返回原始 SQL
            return sql;
        }
        return sql;
    }

    /** 构建权限表达式 */
    private Expression buildPermissionExpression(DataPermissionContext.DataScope scope) {
        DataPermissionContext.DataScopeType type = scope.getType();
        Column column = new Column("dept_id");

        switch (type) {
            case DEPT_TREE:
                // dept_id IN (1, 2, 3, 4)
                InExpression inExpr = new InExpression();
                inExpr.setLeftExpression(column);
                ItemsList itemsList = new ExpressionList(
                    scope.getDeptTree().stream()
                        .map(StringValue::new)
                        .collect(Collectors.toList())
                );
                inExpr.setRightItemsList(itemsList);
                return inExpr;

            case DEPT:
                // dept_id = 'current_dept'
                return new EqualsTo(column, new StringValue(scope.getDeptId()));

            case SELF:
                // create_by = 'user_id'
                return new EqualsTo(new Column("create_by"), new StringValue(scope.getUserId()));

            default:
                return new NullValue();
        }
    }

    private boolean isIgnorePermission(MappedStatement ms) {
        try {
            String className = ms.getId().substring(0, ms.getId().lastIndexOf("."));
            String methodName = ms.getId().substring(ms.getId().lastIndexOf(".") + 1);
            Class<?> clazz = Class.forName(className);
            Method method = null;
            for (Method m : clazz.getMethods()) {
                if (m.getName().equals(methodName)) {
                    method = m;
                    break;
                }
            }
            return method != null && method.isAnnotationPresent(IgnoreDataPermission.class);
        } catch (Exception e) {
            return false;
        }
    }

    private MappedStatement copyMappedStatement(MappedStatement original, String newSql, BoundSql oldBoundSql) {
        BoundSql newBoundSql = new BoundSql(
            original.getConfiguration(),
            newSql,
            oldBoundSql.getParameterMappings(),
            oldBoundSql.getParameterObject()
        );

        MappedStatement.Builder builder = new MappedStatement.Builder(
            original.getConfiguration(),
            original.getId(),
            new BoundSqlSqlSource(newBoundSql),
            original.getSqlCommandType()
        );
        builder.resource(original.getResource());
        builder.fetchSize(original.getFetchSize());
        builder.statementType(original.getStatementType());
        builder.parameterMap(original.getParameterMap());
        builder.resultMaps(original.getResultMaps());
        builder.cache(original.getCache());
        builder.useCache(original.isUseCache());
        return builder.build();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties props) {
        // 可配置忽略权限的 Mapper 方法列表
    }

    private static class BoundSqlSqlSource implements SqlSource {
        private final BoundSql boundSql;
        BoundSqlSqlSource(BoundSql sql) { this.boundSql = sql; }
        @Override
        public BoundSql getBoundSql(Object parameterObject) { return boundSql; }
    }
}

忽略权限注解

Java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreDataPermission {
}
Java
public interface OrderMapper {

    // 正常受数据权限约束
    List<Order> selectOrders(@Param("status") String status);

    // 后台管理:忽略数据权限,查看所有数据
    @IgnoreDataPermission
    List<Order> selectAllOrdersForAdmin();
}

拦截器链与执行顺序

MyBatis 支持多个拦截器叠加,数据权限拦截器需与其他拦截器(如租户拦截器、分页拦截器)正确协作。

Java
拦截器链执行顺序
┌─────────────────────────────────────┐
│  TenantInterceptor (租户过滤)        │  ← 先执行,缩小到租户范围
│    └─ inject: WHERE tenant_id = 'A' │
│         ↓                           │
│  DataPermissionInterceptor (数据权限)│  ← 后执行,在租户内再过滤
│    └─ inject: AND dept_id IN (...)  │
│         ↓                           │
│  PaginationInterceptor (分页)        │  ← 最后执行,添加 LIMIT
│    └─ inject: LIMIT ?, ?            │
│         ↓                           │
│  原始 SQL 执行                      │
└─────────────────────────────────────┘

注册顺序决定执行顺序

Java
@Configuration
public class MyBatisConfig {

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> {
            // 注册顺序 = 拦截顺序
            configuration.addInterceptor(new TenantInterceptor());         // 1. 租户
            configuration.addInterceptor(new DataPermissionInterceptor()); // 2. 数据权限
            configuration.addInterceptor(new PaginationInterceptor());     // 3. 分页
        };
    }
}

拦截器按注册顺序依次执行,SQL 被逐层改写,最终形成完整语句。租户过滤必须先于数据权限过滤。

权限上下文填充

Spring MVC 拦截器

text
@Component
public class DataPermissionInterceptor implements HandlerInterceptor {

    @Autowired
    private UserService userService;

    @Autowired
    private DeptService deptService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        LoginUser user = SecurityUtils.getCurrentUser();
        if (user == null) {
            return true;
        }

        DataPermissionContext.DataScope scope = buildDataScope(user);
        DataPermissionContext.setScope(scope);
        return true;
    }

    private DataPermissionContext.DataScope buildDataScope(LoginUser user) {
        DataPermissionContext.DataScopeType type = resolveScopeType(user);

        DataPermissionContext.DataScope.Builder builder = new DataPermissionContext.DataScope.Builder();
        builder.userId(user.getId());
        builder.deptId(user.getDeptId());
        builder.type(type);

        if (type == DataPermissionContext.DataScopeType.DEPT_TREE) {
            builder.deptTree(deptService.getSubDeptIds(user.getDeptId()));
        }

        return builder.build();
    }

    private DataPermissionContext.DataScopeType resolveScopeType(LoginUser user) {
        // 根据角色返回不同的数据权限类型
        if (user.hasRole("ADMIN")) return DataPermissionContext.DataScopeType.ALL;
        if (user.hasRole("DEPT_MGR")) return DataPermissionContext.DataScopeType.DEPT_TREE;
        if (user.hasRole("DEPT_LEAD")) return DataPermissionContext.DataScopeType.DEPT;
        return DataPermissionContext.DataScopeType.SELF;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) {
        DataPermissionContext.clear();
    }
}

多表 JOIN 场景处理

JOIN 查询中需要明确指定哪个表的列进行过滤:

text
private Expression buildPermissionExpressionForJoin(
        DataPermissionContext.DataScope scope, PlainSelect plainSelect) {

    // 获取主表别名
    String mainTableAlias = extractMainTableAlias(plainSelect);
    String column = mainTableAlias != null ? mainTableAlias + ".dept_id" : "dept_id";
    Column col = new Column(column);

    // 构建 IN 表达式
    InExpression inExpr = new InExpression();
    inExpr.setLeftExpression(col);
    inExpr.setRightItemsList(new ExpressionList(
        scope.getDeptTree().stream()
            .map(StringValue::new)
            .collect(Collectors.toList())
    ));
    return inExpr;
}

注意事项

  1. SQL 解析失败降级:JSqlParser 解析失败时应返回原始 SQL 并记录告警日志,不可直接抛异常阻断业务
  2. 忽略权限注解:后台管理、统计报表等场景使用 @IgnoreDataPermission 绕过数据权限,但需审计日志记录
  3. 拦截器顺序:数据权限拦截器必须在租户拦截器之后、分页拦截器之前注册
  4. 性能开销:每次 SQL 执行都需 AST 解析 + 重写,生产环境建议对相同 SQL 模板进行缓存
  5. INSERT/UPDATE/DELETE:数据权限主要约束查询,写操作应在业务层做权限校验,避免拦截器误拦截

要点总结

  • 数据权限在角色权限基础上增加数据范围约束,实现"同角色、不同数据"的精细化控制
  • 五种权限模型:全部数据、本部门及下级、本部门、本人、自定义,对应不同的 WHERE 条件
  • 使用 JSqlParser 将 SQL 解析为 AST,在 PlainSelect.where 中注入 AND 权限条件,避免字符串替换的精度问题
  • 拦截器链按注册顺序执行:租户过滤 → 数据权限过滤 → 分页,逐层改写 SQL
  • 通过 @IgnoreDataPermission 注解标识豁免数据权限的方法,用于后台管理场景
  • 数据权限上下文通过 Spring MVC 拦截器在 preHandle 中填充,在 afterCompletion 中清理

存放路径:D:\git2\jwdev\articles\MYBATIS\专家\多租户与数据权限\数据权限拦截器.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 多租户架构设计
下一篇 → 租户隔离策略
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库