SpringBoot数据权限控制:注解+动态SQL实践
发布时间:2026/9/16 12:01:09 作者:尧图编辑部 阅读量:1,286

1. 项目概述当数据权限遇上SpringBoot最近在重构公司内部管理系统时我又遇到了那个老生常谈的问题——数据权限控制。每次新功能开发都要在Service层写一堆if-else来判断当前用户能看到哪些数据不仅代码臃肿还容易遗漏权限判断。直到尝试了注解动态SQL的方案后我才发现原来数据权限可以如此优雅地实现。这个方案的核心思想是通过自定义注解标记需要数据权限控制的方法利用MyBatis拦截器动态修改SQL语句自动注入权限过滤条件。实测下来原先需要几十行判断逻辑的查询方法现在只需要加个注解就能搞定代码量减少了70%以上。更重要的是权限规则统一维护在一个地方再也不用担心不同开发人员实现不一致导致的权限漏洞。2. 核心设计思路拆解2.1 传统方案的痛点分析在采用新方案前我们项目中的数据权限实现大致是这样的public ListOrder queryOrders(OrderQuery query) { // 基础查询条件 ListOrder orders orderMapper.selectByQuery(query); // 数据权限过滤 User currentUser SecurityUtils.getCurrentUser(); if (!currentUser.isAdmin()) { if (currentUser.isDeptManager()) { orders orders.stream() .filter(o - o.getDeptId().equals(currentUser.getDeptId())) .collect(Collectors.toList()); } else { orders orders.stream() .filter(o - o.getCreateBy().equals(currentUser.getUserId())) .collect(Collectors.toList()); } } return orders; }这种实现方式存在几个明显问题业务代码和数据权限代码高度耦合可读性差同样的权限逻辑要在多个方法中重复编写先查后过滤的方式性能低下特别是数据量大时权限规则变更需要修改多处代码维护成本高2.2 注解动态SQL方案的优势新方案通过以下方式解决了上述问题声明式编程使用注解声明方法需要的数据权限类型业务代码保持简洁统一处理通过MyBatis拦截器集中处理权限逻辑避免代码重复SQL注入在SQL执行前动态添加WHERE条件实现真正的数据库层过滤规则可配置权限规则可集中配置修改时只需调整一处DataPermission(deptAlias o, userAlias o) public ListOrder queryOrders(OrderQuery query) { // 无需手动处理权限方法保持简洁 return orderMapper.selectByQuery(query); }3. 核心实现细节3.1 自定义注解设计首先定义数据权限注解用于标记需要权限控制的方法Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface DataPermission { /** * 部门表别名 */ String deptAlias() default ; /** * 用户表别名 */ String userAlias() default ; /** * 权限类型 */ DataPermissionType type() default DataPermissionType.ALL; } public enum DataPermissionType { ALL, // 所有数据 DEPT, // 本部门数据 SELF, // 仅本人数据 CUSTOM // 自定义规则 }3.2 MyBatis拦截器实现核心拦截器负责解析注解并修改SQLIntercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class DataPermissionInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取当前执行的Mapper方法 Method method getMapperMethod(invocation); // 2. 检查是否有DataPermission注解 DataPermission permission method.getAnnotation(DataPermission.class); if (permission null) { return invocation.proceed(); } // 3. 获取当前用户权限信息 User currentUser SecurityUtils.getCurrentUser(); if (currentUser.isAdmin()) { return invocation.proceed(); // 管理员跳过权限过滤 } // 4. 解析原始SQL并添加权限条件 StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); String originalSql boundSql.getSql(); String permissionSql buildPermissionSql(originalSql, permission, currentUser); resetSql(handler, boundSql, permissionSql); return invocation.proceed(); } private String buildPermissionSql(String originalSql, DataPermission permission, User user) { // 根据权限类型构建不同的WHERE条件 StringBuilder condition new StringBuilder(); switch (permission.type()) { case DEPT: condition.append(permission.deptAlias()).append(.dept_id ).append(user.getDeptId()); break; case SELF: condition.append(permission.userAlias()).append(.create_by ).append(user.getUserId()).append(); break; case CUSTOM: condition.append(buildCustomCondition(user)); break; default: return originalSql; } // 将条件注入到SQL中 if (originalSql.toUpperCase().contains( WHERE )) { return originalSql.replaceFirst((?i) WHERE , WHERE ( condition ) AND ); } else { int index originalSql.toUpperCase().indexOf( FROM ); String beforeFrom originalSql.substring(0, index); String afterFrom originalSql.substring(index); return beforeFrom afterFrom.replaceFirst((?i) FROM , WHERE condition FROM ); } } }3.3 权限上下文传递为了在拦截器中获取当前用户信息我们需要实现一个线程安全的权限上下文public class SecurityUtils { private static final ThreadLocalUser userHolder new ThreadLocal(); public static User getCurrentUser() { User user userHolder.get(); if (user null) { throw new IllegalStateException(No user in current context); } return user; } public static void setCurrentUser(User user) { userHolder.set(user); } public static void clear() { userHolder.remove(); } }注意记得在过滤器或拦截器中清理ThreadLocal否则可能导致内存泄漏4. 高级功能扩展4.1 多表关联权限控制对于需要关联多表的复杂查询可以通过注解指定每个表的权限别名DataPermission( deptAlias {o, c}, // 订单和客户表都需要部门权限 userAlias o ) public ListOrderDTO queryOrderDetails(OrderQuery query) { return orderMapper.selectOrderDetails(query); }对应的SQL修改逻辑需要处理多个表的权限条件private String buildMultiTablePermission(String originalSql, DataPermission permission, User user) { ListString deptConditions new ArrayList(); for (String alias : permission.deptAlias()) { if (!alias.isEmpty()) { deptConditions.add(alias .dept_id user.getDeptId()); } } ListString userConditions new ArrayList(); for (String alias : permission.userAlias()) { if (!alias.isEmpty()) { userConditions.add(alias .create_by user.getUserId() ); } } // 组合所有条件 String condition Stream.concat(deptConditions.stream(), userConditions.stream()) .collect(Collectors.joining( OR )); return injectCondition(originalSql, ( condition )); }4.2 权限规则动态配置将硬编码的权限规则改为从数据库或配置中心读取Service public class DataPermissionRuleService { Cacheable(value permissionRules, key #roleId) public ListDataPermissionRule getRulesByRole(String roleId) { // 从数据库查询该角色对应的数据权限规则 return dataPermissionRuleMapper.selectByRole(roleId); } } // 在拦截器中使用 ListDataPermissionRule rules ruleService.getRulesByRole(currentUser.getRoleId()); String condition rules.stream() .map(rule - rule.getTableAlias() . rule.getColumn() rule.getOperator() rule.getValue()) .collect(Collectors.joining( AND ));4.3 性能优化技巧SQL解析优化使用JSqlParser等工具替代字符串操作更可靠地修改SQLStatement statement CCJSqlParserUtil.parse(sql); Select select (Select) statement; PlainSelect plainSelect (PlainSelect) select.getSelectBody(); // 添加权限条件 Expression where plainSelect.getWhere(); if (where null) { plainSelect.setWhere(new Parenthesis(new AndExpression(permissionCondition))); } else { plainSelect.setWhere(new Parenthesis(new AndExpression(where, permissionCondition))); } return select.toString();缓存权限SQL对相同的SQL模板和权限组合进行缓存private static final CacheString, String SQL_CACHE Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); String cacheKey originalSql | currentUser.getRoleId(); String permissionSql SQL_CACHE.get(cacheKey, k - buildPermissionSql(originalSql, permission, currentUser));5. 常见问题与解决方案5.1 分页总数问题问题描述当使用PageHelper等分页插件时权限条件只应用到了分页查询SQL没有应用到count查询SQL导致分页总数不正确。解决方案修改拦截器同时处理原始SQL和countSQLif (BoundSqlHelper.isCountSql(boundSql)) { String countSql boundSql.getSql(); String permissionCountSql buildPermissionSql(countSql, permission, currentUser); resetSql(handler, boundSql, permissionCountSql); } else { String permissionSql buildPermissionSql(originalSql, permission, currentUser); resetSql(handler, boundSql, permissionSql); }5.2 多数据源支持问题描述项目中使用多个数据源时拦截器需要对特定数据源生效。解决方案通过ConditionalOnProperty或自定义条件装配拦截器ConditionalOnProperty(name spring.datasource.primary.enable-data-permission, havingValue true) Bean public DataPermissionInterceptor dataPermissionInterceptor() { return new DataPermissionInterceptor(); }5.3 权限条件冲突问题描述手动编写的WHERE条件可能与自动注入的权限条件产生逻辑冲突。解决方案使用括号明确条件分组确保权限条件的独立性-- 原始SQL SELECT * FROM orders WHERE status ACTIVE AND create_time 2023-01-01 -- 修改后SQL SELECT * FROM orders WHERE (status ACTIVE AND create_time 2023-01-01) AND (create_by user123 OR dept_id dept456)6. 最佳实践建议注解使用原则保持注解配置最小化只声明必要的属性为常用查询创建专门的权限注解如OrderPermission、CustomerPermission等避免在Controller层使用数据权限注解保持权限控制靠近数据层测试策略对拦截器进行单元测试验证各种SQL场景下的修改正确性编写集成测试模拟不同权限用户查询数据的结果使用AOP测试工具验证注解是否按预期生效监控与日志记录SQL修改前后的对比日志仅开发环境监控权限拦截器的执行时间确保不会成为性能瓶颈实现权限命中率统计了解各权限规则的使用频率灰度发布方案先在小范围功能中试点新权限方案保留旧权限代码通过开关控制新旧方案切换对比新旧方案的查询结果确保一致性这套方案在我们生产环境运行半年多以来数据权限相关的Bug减少了90%以上新功能开发时也不再需要反复确认权限逻辑是否正确实现。特别是在应对组织架构调整时只需修改权限规则配置所有相关查询自动适应新的权限要求真正实现了一次编写处处生效的理想效果。