一、背景与痛点分析

1.1 真实业务场景

角色 数据访问范围 传统方案痛点
普通员工 仅自己创建的任务 每个查询方法重复编写WHERE user_id=?
部门经理 本部门所有任务 需关联部门表并动态拼接dept_id
系统管理员 全量数据 权限逻辑与业务代码深度耦合

1.2 传统方案的致命缺陷

        

// 业务代码中混杂权限逻辑(反面示例)
public List<Task> listTasks() {
    String sql = "SELECT * FROM tb_task";
    if (currentUser.isStaff()) {
        sql += " WHERE user_id = " + currentUser.getId(); // SQL注入风险!
    } else if (currentUser.isManager()) {
        sql += " JOIN dept ON..."; // 复杂度爆炸式增长
    }
    // ...
}

痛点归纳

  • 代码重复率高
  • SQL注入风险
  • 复杂查询难以维护
  • 权限变更需修改业务代码

二、解决方案设计

2.1 核心思路

        利用 MyBatis 的拦截器机制,在 SQL 执行之前对其进行拦截和修改,根据当前用户的权限信息动态地添加数据过滤条件。通过在实体类和方法上添加自定义注解,标记需要进行权限控制的表和字段,在拦截器中解析这些注解,并根据用户的权限信息生成相应的 SQL 过滤条件,从而实现对数据的动态权限控制。

2.2 核心架构

2.3、四大核心模板

模块 组件 职责说明
注解模块 @ResourceCheck 标记需权限控制的接口
@ResourceTableCheck 标记需权限控制的实体类
@ResourceFieldCheck 定义字段过滤规则(如USER_IDDEPT_PATH
拦截器 ResourceCodeInterceptor SQL解析与条件注入核心
配置模块 PermissionProperties 动态开关权限/配置白名单
上下文 UserContextHolder 基于ThreadLocal存储用户信息

三、代码实现细节

3.1 注解模块

通过三个核心注解实现权限规则的声明式配置:

// 标记需要权限控制的方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceCheck {
}

// 标记需要权限控制的实体类(表)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceTableCheck {
}

// 标记需要权限控制的字段及过滤规则
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceFieldCheck {
    ResoureFieldType code();         // 字段类型(用户ID、部门路径等)
    SqlKeyword compare() default SqlKeyword.EQ; // 比较操作符
}

3.2 拦截器模块

执行流程

3.2.1 权限校验拦截器

@Aspect
@Component
public class ResourceCheckAspect {
    @Pointcut("@annotation(com.dynamic.data.annotation.ResourceCheck)")
    public void annotationPointcut() {}
    
    @Before("annotationPointcut()")
    public void beforeAdvice() {
        ResourceCheckHolder.set(true); // 标记需要权限校验
    }
    
    @After("annotationPointcut()")
    public void afterAdvice() {
        ResourceCheckHolder.clear(); // 清除标记
    }
}

3.2.2 SQL 改写拦截器(核心)

@Intercepts({
    @Signature(type = StatementHandler.class, 
              method = "prepare", 
              args = {Connection.class, Integer.class})
})
public class ResourceCodeInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) {
        // 1. 检查是否需要权限校验
        if (!Boolean.TRUE.equals(ResourceCheckHolder.get())) {
            return invocation.proceed();
        }
        
        // 2. 获取原始SQL并解析
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        BoundSql boundSql = statementHandler.getBoundSql();
        String originalSql = boundSql.getSql();
        
        // 3. 动态添加权限过滤条件
        String modifiedSql = addResourceCodeCondition(originalSql);
        PluginUtils.mpBoundSql(boundSql).sql(modifiedSql);
        
        return invocation.proceed();
    }
    
    private String addResourceCodeCondition(String originalSql) {
        // 使用JSqlParser解析SQL
        Statement statement = CCJSqlParserUtil.parse(originalSql);
        
        // 处理不同类型的查询(SELECT/JOIN/SUBQUERY等)
        if (statement instanceof Select) {
            processSelect((Select) statement);
        }
        
        return statement.toString();
    }
}

3.3 配置模块

3.3.1 PermissionProperties 配置类

@ConfigurationProperties(prefix = "permission")
public class PermissionProperties {
    private boolean enabled = true;          // 是否开启权限校验
    private String prefix;                   // 拦截接口前缀
    private List<String> excludePathPatterns;// 排除不校验的路径
    private String userServiceUrl;           // 用户服务地址
}

PermissionProperties 配置类用于配置权限校验的相关参数,如是否开启权限校验、排除不校验的路径、用户系统后端地址等。

3.3.2 WebMvcConfig 配置类

@Configuration
@EnableConfigurationProperties({PermissionProperties.class, ServerProperties.class})
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    @ConditionalOnMissingBean
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor();
    }

    @Bean
    @ConditionalOnMissingBean
    public SpringContextUtil springContextUtil() {
        return new SpringContextUtil();
    }

    @Bean
    @ConditionalOnMissingBean
    public ResourceCodeInterceptor resourceCodeInterceptor() {
        return new ResourceCodeInterceptor();
    }

    @Bean
    @ConditionalOnMissingBean
    public ResourceCheckAspect resourceCheckAspect() {
        return new ResourceCheckAspect();
    }

    @Autowired
    private PermissionProperties permissionProperties;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加请求拦截器
        if (permissionProperties.isEnabled()) {
            registry.addInterceptor(requestInterceptor()).excludePathPatterns(permissionProperties.getExcludePathPatterns());
        }
    }
}

WebMvcConfig 配置类用于配置拦截器和 Spring 上下文工具类,将请求拦截器和 MyBatis 拦截器注册到 Spring 容器中,并根据配置决定是否启用请求拦截器。

在 spring.factories 文件中加入以下配置:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx.xxx.xxx.xxx.config.WebMvcConfig

将 WebMvcConfig 类声明为 Spring Boot 的自动配置类。当应用启动时,Spring Boot 会自动加载并实例化。

3.4 用户信息模块

线程安全的上下文管理,存储用户信息

public class UserContextHolder {
    private static final ThreadLocal<UserInfoContext> USER_INFO_THREAD_LOCAL = new ThreadLocal<>();
    
    public static void set(UserInfoContext userInfoContext) {
        USER_INFO_THREAD_LOCAL.set(userInfoContext);
    }
    
    public static UserInfoContext get() {
        return USER_INFO_THREAD_LOCAL.get();
    }
    
    public static void clear() {
        USER_INFO_THREAD_LOCAL.remove();
    }
}

四、代码调用示例

4.1 实体类定义

@ResourceTableCheck
@Getter
@Setter
@NoArgsConstructor
@TableName("tb_task")
public class TbTask {

    // 基础字段...

    @ResourceFieldCheck(code = ResoureFieldType.USER_ID)
    @TableField(value = "user_id", keepGlobalFormat = true)
    private Long userId;

    @ResourceFieldCheck(code = ResoureFieldType.START_TIME, compare = SqlKeyword.GE)
    @TableField(value = "create_time", keepGlobalFormat = true)
    private Date createTime;

}

在实体类 TbTask 上添加 ResourceTableCheck 注解,表示该表需要进行权限控制。在 userId 和 createTime 字段上添加 ResourceFieldCheck 注解,指定了查询的字段类型和查询条件。

4.2 控制器定义

@RestController
@RequestMapping("/demo")
public class DemoController {
    @Autowired
    private TaskService taskService;
    
    // 无需权限校验的接口
    @GetMapping("test1")
    public void test1() {
        taskService.list();
    }
    
    // 需要权限校验的接口
    @ResourceCheck // 核心注解,开启权限控制
    @GetMapping("test2")
    public void test2() {
        taskService.list();
    }
}

4.3 SQL 转换效果

原始 SQL

SELECT  id,task_name,task_detail,user_id,create_time  FROM tb_task

转换后 SQL

SELECT id, task_name, task_detail, user_id, create_time FROM tb_task WHERE tb_task.user_id = 1000 AND tb_task.create_time >= '2025-06-17 14:04:54'

五、方案优势与应用场景

5.1 核心优势

  1. 零侵入性:业务代码无需添加任何权限逻辑,DAO/Service 层保持纯净
  2. 声明式配置:通过注解即可完成权限规则定义,新增表只需添加注解
  3. 动态灵活性
    • 支持多种比较操作符(=, >, IN, BETWEEN 等)
    • 支持多字段组合条件与复杂 SQL(JOIN / 子查询等)
  4. 安全增强
    • 自动拼接 WHERE 条件,防止 SQL 注入
    • 线程安全的上下文管理
  5. 维护便捷:权限规则变更只需修改注解配置,无需修改业务代码

5.2 扩展应用场景

  1. 多租户隔离:通过租户 ID 字段实现数据隔离
  2. 数据时效控制:基于有效时间字段实现数据可见性控制
  3. 部门数据隔离:通过部门路径字段实现分级数据访问
  4. 混合权限策略:支持多维度权限组合(区域 + 销售级别等)

六、总结

本文提出的基于 MyBatis 拦截器的动态数据权限方案,通过巧妙的注解体系与 SQL 解析技术,实现了:

  • 声明式权限配置:通过注解声明权限规则

  • 自动化 SQL 改写:动态注入权限条件

  • 上下文驱动:基于用户会话自动过滤

  • 多维扩展能力:支持多种权限控制场景

相比传统硬编码方案,该方案减少权限相关代码量 80% 以上,极大提升系统的可维护性和安全性。

:完整实现代码已开源:https://gitcode.com/weixin_44813108/dynamic-data-permission

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐