告别硬编码!Spring Boot 3 + MyBatis-Plus 优雅实现数据权限控制无处不在的数据权限之痛

  在日常的企业级应用开发中,你是否经常遇到这样的场景:

  1. 权限代码泛滥:在每一个数据查询的 Service 或 Mapper 方法里,都充斥着 if-else 判断和手动拼接的 WHERE 条件,代码重复且难以维护。
  2. 安全风险隐匿:某个查询一旦忘记添加权限过滤,便可能导致敏感数据泄露,而这种疏漏在代码审查中极难被发现。
  3. 需求变更噩梦:当权限规则从“只能看自己的数据”变更为“可看本部门及下属部门数据”时,你需要满世界寻找并修改所有相关的SQL逻辑。
  4. 接口职责混乱:业务方法本应只关心核心业务逻辑,却被迫承载了大量数据访问控制的职责,违反了单一职责原则。

  这些问题的核心在于:数据权限的控制逻辑,与业务逻辑高度耦合,并以“硬编码”的形式散落在系统的各个角落。

什么是数据权限?为何传统方案乏力?

  数据权限,是区别于功能权限(能否访问某个菜单或按钮)的更深层次的权限控制。它决定了一个用户在拥有功能访问权的前提下,能看到哪些范围的数据。常见的模型包括:

  • 个人数据:用户只能操作自己创建的数据。
  • 部门数据:用户可以操作其所属部门的所有数据。
  • 部门及子部门数据:用户可以操作其所属部门及其所有下级部门的数据。
  • 自定义数据范围:根据复杂的组织架构和角色动态计算数据范围。

      传统方案的弊端:

    1. 在业务层拼接参数:在 Service 层根据用户身份计算好数据ID列表,传递给 Mapper。这导致业务层臃肿,且无法应对复杂的动态SQL场景。
    2. 在Mapper层硬编码:在 MyBatis 的XML文件中,使用``标签或直接编写${}进行字符串替换。这种方式极不安全(存在SQL注入风险),且无法实现通用的、解耦的方案。

      显然,我们需要一种非侵入式、集中化管理、动态灵活的解决方案。而 MyBatis-Plus 作为 MyBatis 的强力增强工具,其提供的数据权限插件(DataPermissionHandler) 正是为此而生。结合 Spring Boot 3 的现代化特性,我们可以构建出一个非常优雅的解决方案。

    基于MyBatis-Plus插件实现动态数据过滤

      本方案的核心思想是:通过MyBatis-Plus插件,在SQL执行前,动态且自动地在查询语句上附加数据权限过滤条件。

    第一步:环境准备与依赖引入

      确保你的项目是基于 Spring Boot 3.x 和 MyBatis-Plus 3.5.0+(较高版本对该功能支持更完善)。

       com.baomidou mybatis-plus-boot-starter 3.5.7 第二步:定义数据权限注解

      我们首先定义一个注解,用于在 Mapper 方法上声明此方法需要何种数据权限。这是实现精细化控制和解耦的关键。

      /** * 数据权限注解 * 可标注在Mapper类或方法上,用于声明需要进行数据权限过滤 */@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface DataPermission { /** * 数据权限字段名(默认表中表示部门ID的字段名为`dept_id`) */ String deptAlias() default "dept_id"; /** * 用户ID字段名(默认表中表示创建用户的字段名为`user_id`) */ String userAlias() default "user_id"; /** * 权限类型 */ DataScopeType type() default DataScopeType.ALL;}/** * 数据权限范围类型枚举 */public enum DataScopeType { ALL, // 所有数据 DEPT_AND_SUB, // 本部门及子部门 DEPT, // 本部门 SELF; // 仅本人数据}第三步:实现核心—自定义DataPermissionHandler

      这是整个方案的“大脑”。它需要实现 MyBatis-Plus 的 DataPermissionHandler 接口,根据当前登录用户的信息和 @DataPermission 注解的配置,动态生成 WHERE 条件片段。

      @Componentpublic class MyDataPermissionHandler implements DataPermissionHandler { @Autowired private UserContext userContext; // 假设已存在,用于获取当前登录用户信息 @Override public Expression getSqlSegment(Expression where, String mappedStatementId) { // 1. 获取当前执行的Mapper方法 MappedStatement ms = SqlHelper.getMappedStatement(mappedStatementId); Object annotation = getAnnotation(ms, DataPermission.class); if (annotation == null) { return where; // 没有注解,不做过滤 } DataPermission dataPermission = (DataPermission) annotation; // 2. 获取当前用户权限上下文 Long currentUserId = userContext.getCurrentUserId(); Long currentDeptId = userContext.getCurrentDeptId(); List deptIdScope = userContext.getCurrentDeptIdScope(); // 本部门及所有子部门ID列表 // 3. 根据注解类型,构建不同的条件 MixedSqlExpression sqlSegment = null; DataScopeType type = dataPermission.type(); switch (type) { case SELF: // 例如: user_id = 123 sqlSegment = new MixedSqlExpression( new Column(dataPermission.userAlias()), “=”, new NumericValue(currentUserId.toString()) ); break; case DEPT: // 例如: dept_id = 456 sqlSegment = new MixedSqlExpression( new Column(dataPermission.deptAlias()), “=”, new NumericValue(currentDeptId.toString()) ); break; case DEPT_AND_SUB: // 例如: dept_id IN (456, 789, 101112) if (deptIdScope != null && !deptIdScope.isEmpty()) { List idValues = deptIdScope.stream() .map(id -> new NumericValue(id.toString())) .collect(Collectors.toList()); sqlSegment = new InExpression(new Column(dataPermission.deptAlias()), idValues); } break; case ALL: default: // 不做过滤 return where; } // 4. 将构建的权限条件与原WHERE条件用AND连接 if (where == null) { return sqlSegment; } return new AndExpression(where, sqlSegment); } // 辅助方法:通过反射获取注解 private Object getAnnotation(MappedStatement ms, Class< extends Annotation> annotationClass) { // ... 实现逻辑:从Mapper接口对应的方法上获取@DataPermission注解 }}第四步:配置插件并注入Spring

      将我们自定义的 Handler 配置到 MyBatis-Plus 的拦截器链中。

      @Configurationpublic class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(MyDataPermissionHandler dataPermissionHandler) { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加数据权限插件 DataPermissionInterceptor dataPermissionInterceptor = new DataPermissionInterceptor(); dataPermissionInterceptor.setDataPermissionHandler(dataPermissionHandler); interceptor.addInnerInterceptor(dataPermissionInterceptor); // 可以继续添加其他插件,如分页插件、乐观锁插件 // interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; }}第五步:在Mapper层进行声明式使用

      现在,在需要进行数据权限控制的 Mapper 方法上,使用我们定义的 @DataPermission 注解即可,业务层代码无需任何改动。

      @Mapperpublic interface OrderMapper extends BaseMapper { // 示例1:查询本部门订单 @DataPermission(type = DataScopeType.DEPT) List selectDeptOrders(Map params); // 示例2:查询本人订单(自动添加 user_id = 当前用户ID 条件) @DataPermission(type = DataScopeType.SELF) List selectMyOrders(Map params); // 示例3:复杂查询,同时关联其他表,注解可指定表别名 @DataPermission(type = DataScopeType.DEPT_AND_SUB, deptAlias = "o.dept_id") List selectComplexOrdersWithPermission();}

      至此,一个优雅、解耦、声明式的数据权限控制方案就已搭建完成。 任何使用 @DataPermission 注解的查询,都会在运行时自动、无缝地注入对应的数据过滤条件。

    总结

      彻底解耦:权限控制逻辑从业务代码中剥离,集中到 DataPermissionHandler 和注解中,符合设计原则。

      声明式编程:通过在 Mapper 方法上添加注解即可启用控制,使用简单,意图清晰。

      安全无侵入:基于MyBatis-Plus插件机制,在SQL引擎层进行安全的条件追加,避免了SQL注入风险。

      灵活可扩展:DataPermissionHandler 中的逻辑可以根据你的组织架构和权限模型任意扩展,轻松支持更复杂的场景。

      维护成本低:当权限规则变化时,通常只需修改 Handler 中的一处逻辑,极大提升了可维护性。

      数据权限控制是构建安全、可靠企业应用的基石。与其继续在无穷尽的 if-else 和SQL拼接中挣扎,不如立即尝试将这套方案应用到你的 Spring Boot 3 项目中。

      你是否在项目中遇到过更复杂的数据权限场景?例如,基于用户角色动态切换权限规则,或处理多租户(SaaS)场景下的数据隔离?欢迎在评论区分享你的经验和见解,让我们共同探讨更优的架构设计方案。