MyBatis拦截器黑科技:不修改业务代码实现动态数据权限控制

张开发
2026/4/9 2:40:25 15 分钟阅读

分享文章

MyBatis拦截器黑科技:不修改业务代码实现动态数据权限控制
MyBatis拦截器黑科技零侵入实现企业级数据权限管控在当今企业级应用开发中数据权限控制是一个无法回避的核心需求。传统方案往往需要在每个SQL语句中硬编码权限条件或者通过AOP切面批量修改Mapper接口这些方法要么维护成本高要么灵活性不足。而MyBatis拦截器提供了一种优雅的解决方案——无需修改业务代码通过动态SQL改写实现精细化的数据权限控制。1. MyBatis拦截器机制深度解析MyBatis拦截器本质上基于JDK动态代理和责任链模式允许开发者在SQL执行的关键节点插入自定义逻辑。与Spring AOP不同它直接作用于MyBatis核心组件提供了更底层的控制能力。1.1 四大可拦截组件及其生命周期MyBatis开放了四个核心组件的拦截点构成完整的SQL执行流水线组件类型拦截时机典型应用场景Executorupdate/query/commit等操作执行前后缓存控制、事务管理、SQL执行监控ParameterHandler参数设置阶段参数加密/解密、参数校验ResultSetHandler结果集处理阶段结果集二次加工、敏感数据脱敏StatementHandlerSQL准备和执行阶段SQL重写、分页处理、动态字段控制1.2 拦截器核心API工作原理每个自定义拦截器需要实现Interceptor接口的三个关键方法public interface Interceptor { // 核心拦截逻辑 Object intercept(Invocation invocation) throws Throwable; // 生成代理对象 Object plugin(Object target); // 读取配置参数 void setProperties(Properties properties); }其中plugin方法的标准实现应该始终使用MyBatis提供的工具类Override public Object plugin(Object target) { return Plugin.wrap(target, this); }Plugin.wrap()方法内部会检查目标对象是否符合Intercepts注解定义的拦截条件只有匹配时才会创建代理对象这种设计避免了不必要的代理开销。2. 动态数据权限实战方案2.1 基于ThreadLocal的权限参数传递企业级应用中权限参数通常来自当前用户上下文。我们通过ThreadLocal实现线程安全的参数传递public class DataPermissionContext { private static final ThreadLocalPermissionParam CONTEXT new ThreadLocal(); public static void set(PermissionParam param) { CONTEXT.set(param); } public static PermissionParam get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } // 权限参数封装类 Data public static class PermissionParam { private Long userId; private Long deptId; private ListLong roleIds; // 其他业务字段... } }在拦截器中可以这样获取权限参数PermissionParam permission DataPermissionContext.get(); if (permission null) { throw new IllegalStateException(数据权限上下文未设置); }2.2 SQL动态改写引擎拦截StatementHandler.prepare方法对原始SQL进行智能改写Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class DataPermissionInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); // 获取原始SQL String originalSql boundSql.getSql(); // 解析SQL类型 SqlCommandType sqlType getSqlCommandType(handler); // 动态添加权限条件 String modifiedSql applyDataPermission(originalSql, sqlType); // 通过反射修改SQL Field field boundSql.getClass().getDeclaredField(sql); field.setAccessible(true); field.set(boundSql, modifiedSql); return invocation.proceed(); } private String applyDataPermission(String sql, SqlCommandType sqlType) { PermissionParam permission DataPermissionContext.get(); if (permission null) return sql; // 使用SQL解析器分析语句结构 SQLStatement stmt SQLUtils.parseSingleStatement(sql, JdbcUtils.MYSQL); if (stmt instanceof Select) { return processSelect((Select) stmt, permission); } else if (stmt instanceof Update) { return processUpdate((Update) stmt, permission); } // 其他SQL类型处理... return sql; } }2.3 多租户隔离实现方案对于SaaS系统需要在SQL中自动注入租户隔离条件private String addTenantCondition(Select select, PermissionParam permission) { Table from select.getFrom(); String alias from.getAlias() null ? : from.getAlias() .; // 构造租户条件表达式 BinaryExpression tenantCond new BinaryExpression( alias tenant_id, , String.valueOf(permission.getTenantId()) ); // 合并到WHERE条件 if (select.getWhere() null) { select.setWhere(tenantCond); } else { select.setWhere(new BinaryExpression( select.getWhere(), AND, tenantCond )); } return SQLUtils.toSQLString(select); }3. 高级应用场景与性能优化3.1 行列级权限组合控制实现同时控制可访问的数据行和可见字段public class ColumnPermission { private String tableName; private SetString visibleColumns; private SetString sensitiveColumns; public boolean isColumnAllowed(String column) { return visibleColumns.contains(column.toUpperCase()); } public boolean isSensitiveColumn(String column) { return sensitiveColumns.contains(column.toUpperCase()); } } // 在SQL改写阶段应用列权限 private String applyColumnPermission(Select select, ColumnPermission permission) { select.getSelectList().forEach(item - { if (item instanceof SelectItem) { SelectItem selectItem (SelectItem) item; String column selectItem.getExpr().toString(); if (!permission.isColumnAllowed(column)) { selectItem.setExpr(new SQLIdentifierExpr(NULL)); } else if (permission.isSensitiveColumn(column)) { selectItem.setExpr(new SQLMethodInvokeExpr(MASK, new SQLIdentifierExpr(column))); } } }); return SQLUtils.toSQLString(select); }3.2 性能优化关键点SQL解析缓存使用LRU缓存已解析的SQL语句结构条件短路机制对于超级管理员跳过权限过滤批量操作优化特殊处理批量插入/更新语句动态代理优化减少不必要的代理嵌套// 性能优化后的plugin方法实现 Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; // 非目标类型直接返回避免多余代理 }4. 生产环境最佳实践4.1 完整拦截器实现示例Intercepts(Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class})) Slf4j public class AdvancedDataPermissionInterceptor implements Interceptor { private final SQLParserFeature[] features { SQLParserFeature.EnableSQLBinaryOpExprGroup, SQLParserFeature.UseInsertColumnsCache }; Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); try { StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); // 跳过无需处理的SQL类型 if (shouldSkip(boundSql)) { return invocation.proceed(); } String originalSql boundSql.getSql(); String modifiedSql processSql(originalSql); if (!originalSql.equals(modifiedSql)) { log.debug(SQL rewritten: {}, modifiedSql); resetBoundSql(boundSql, modifiedSql); } return invocation.proceed(); } finally { log.debug(Permission check cost: {}ms, System.currentTimeMillis() - start); } } private String processSql(String sql) { try { SQLStatement stmt SQLUtils.parseSingleStatement(sql, JdbcUtils.MYSQL, features); PermissionParam permission DataPermissionContext.get(); if (permission null || permission.isAdmin()) { return sql; } if (stmt instanceof Select) { return processSelect((Select) stmt, permission); } else if (stmt instanceof Update) { return processUpdate((Update) stmt, permission); } else if (stmt instanceof Delete) { return processDelete((Delete) stmt, permission); } return sql; } catch (Exception e) { log.warn(SQL parse error, skip permission control, e); return sql; } } // 其他工具方法... }4.2 Spring Boot集成配置Configuration public class MyBatisConfig { Bean public DataPermissionInterceptor dataPermissionInterceptor() { DataPermissionInterceptor interceptor new DataPermissionInterceptor(); // 可配置属性注入 Properties properties new Properties(); properties.setProperty(enableColumnPermission, true); interceptor.setProperties(properties); return interceptor; } Bean public ConfigurationCustomizer configurationCustomizer() { return configuration - { // 确保拦截器在分页插件之后执行 configuration.addInterceptor(dataPermissionInterceptor()); }; } }4.3 常见问题排查指南SQL改写失效检查清单确认拦截器已正确注册到Configuration检查Intercepts注解配置是否正确验证ThreadLocal中权限参数是否设置查看MyBatis日志确认最终执行的SQL性能问题定位方法使用Arthas监控拦截器执行耗时检查是否出现SQL解析瓶颈确认没有不必要的重复代理多数据源适配方案Bean Primary public SqlSessionFactory masterSessionFactory( Qualifier(masterDataSource) DataSource dataSource, DataPermissionInterceptor interceptor) throws Exception { SqlSessionFactoryBean bean new SqlSessionFactoryBean(); bean.setDataSource(dataSource); // 其他配置... bean.setPlugins(new Interceptor[]{interceptor}); return bean.getObject(); }在实际项目中这种非侵入式的数据权限方案相比传统方式减少了80%以上的权限相关代码特别是在快速迭代的业务系统中开发人员可以完全专注于业务逻辑实现而不用担心权限控制的传播问题。

更多文章