一个注解轻松实现数据权限

94 阅读9分钟

不知道在座的亦菲、彦祖们有没有遇到这样的需求,要在某块业务加上数据权限控制,规定哪些人能访问,哪些人不能访问。像这样的数据权限的控制该怎么实现呢?可以使用基于Mybatis-Plus的插件机制来实现。话不多说,我们直接切入主题,开始实现

代码

数据库准备
-- 插入部门表
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '部门ID',
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '部门名称',
  `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '部门编码',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '部门描述',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `code`(`code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- 插入用户表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '密码',
  `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '手机号',
  `department_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
-- 插入用户数据
INSERT INTO `user` VALUES (1, 'testuser1', '123456', '13800138001', 1, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (2, 'testuser2', '123456', '13800138002', 2, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (3, 'testuser3', '123456', '13800138003', 3, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (4, 'tech_user1', '123456', '13800138004', 1, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (5, 'tech_user2', '123456', '13800138005', 1, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (6, 'sales_user1', '123456', '13800138006', 2, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (7, 'sales_user2', '123456', '13800138007', 2, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (8, 'hr_user1', '123456', '13800138008', 3, '2026-01-19 20:48:31');
INSERT INTO `user` VALUES (9, 'finance_user1', '123456', '13800138009', 4, '2026-01-19 20:48:31');
​
-- 插入部门数据
INSERT INTO `department` (`id`, `name`, `code`, `description`, `create_time`) VALUES 
(1, '技术部', 'TECH', '技术研发部门', CURRENT_TIMESTAMP),
(2, '销售部', 'SALES', '销售市场部门', CURRENT_TIMESTAMP),
(3, '人力资源部', 'HR', '人力资源管理部门', CURRENT_TIMESTAMP),
(4, '财务部', 'FINANCE', '财务管理部门', CURRENT_TIMESTAMP);
在pom.xml引入依赖
<!--MyBatis-Plus扩展包(用于拦截器功能)-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-extension</artifactId>
    <version>3.5.3.1</version>
</dependency>
<!-- SQL解析器(用于数据权限拦截器) -->
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.6</version>
</dependency>
<!--Lombok(如果使用@Slf4j注解)-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
样例
//user对象
@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String password;
    private String phone;
    private Long departmentId;
    private LocalDateTime createTime;
}
//mapper
@Mapper
public interface UserDao extends BaseMapper<User> {
    @DataPermission //数据权限注解
    @Select("select * from user")
    List<User> selectUserList();
}
​
//service
public interface UserService extends IService<User> {
    List<User> listUsersWithPermission();
}
//serviceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService {
    @Autowired
    private UserDao userDao;
    @Override
    public List<User> listUsersWithPermission() {
        return userDao.selectUserList();
    }
}
​
//controller
@RestController
@RequestMapping("/api/users")
public class UserController {
​
    @Autowired
    private UserService userService;
​
    /**
     * 获取所有用户 - 应用数据权限过滤
     * 只有当前用户所在部门的用户数据会被返回
     */
    @GetMapping
    public List<User> list() {
        return userService.listUsersWithPermission();
    }
}
数据权限注解
  1. 自定义注解 - @DataPermission
  2. 权限拦截器(核心) - DataPermissionInterceptor
  3. Mybatis拦截器容器 - MybatisPlusInterceptor
  4. SQL解析器 - JSqlParser
//注解类
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
​
    /**
     * 部门字段名
     */
    String departmentField() default "department_id";
}
​
//权限拦截器
@Slf4j
@Component
public class DataPermissionInterceptor implements InnerInterceptor {
    
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 获取当前用户信息(这里需要从SecurityContext或其他地方获取当前登录用户)
        Long currentUserDepartmentId = getCurrentUserDepartmentId();
        if (currentUserDepartmentId == null) {
            return; // 如果没有部门信息,不进行数据权限过滤
        }
        // 检查方法是否标注了@DataPermission注解
        String methodName = ms.getId();
        boolean hasDataPermission = hasDataPermissionAnnotation(methodName);
​
        if (!hasDataPermission) {
            return; // 如果没有注解,不进行数据权限过滤
        }
​
        // 解析SQL并添加部门过滤条件
        String originalSql = boundSql.getSql();
        try {
            String modifiedSql = addDepartmentFilter(originalSql, currentUserDepartmentId);
            // 使用反射修改BoundSql中的SQL
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, modifiedSql);
​
            log.info("数据权限拦截器修改SQL: {}", modifiedSql);
        } catch (Exception e) {
            log.error("数据权限拦截器处理SQL失败", e);
        }
    }
​
    /**
     * 获取当前用户的部门ID
     */
    private Long getCurrentUserDepartmentId() {
        //模拟获取当前用户部门ID,实际应该从SecurityContext或ThreadLocal获取
        return 1L;
    }
​
    private Boolean hasDataPermissionAnnotation(String methodName){
        try {
            //通过方法名解析类名和方法名
            int lastDotIndex = methodName.lastIndexOf(".");
            String className = methodName.substring(0, lastDotIndex);
            String methodNameOnly = methodName.substring(lastDotIndex + 1);
            Class<?> clazz = Class.forName(className);
            Method method = clazz.getMethod(methodNameOnly);
            return method.isAnnotationPresent(DataPermission.class);
        }catch (Exception e){
            log.warn("检查数据权限注解失败: {}", methodName, e);
            return false;
        }
    }
​
    /**
     * 添加部门过了条件到SQL中
     */
    private String addDepartmentFilter(String sql,Long departmentId) throws JSQLParserException{
        Select select = (Select) CCJSqlParserUtil.parse(sql);
        SelectBody selectBody = select.getSelectBody();
        if (selectBody instanceof PlainSelect){
            PlainSelect plainSelect = (PlainSelect) selectBody;
            Expression where = plainSelect.getWhere();
​
            Column departmentColumn = new Column("department_id");
            LongValue departmentValue = new LongValue(departmentId);
            EqualsTo departmentFilter = new EqualsTo(departmentColumn, departmentValue);
            if (where ==null){
                plainSelect.setWhere(departmentFilter);
            }else {
                AndExpression andExpression =  new AndExpression(where,departmentFilter);
                plainSelect.setWhere(andExpression);
            }
        }
        return select.toString();
    }
}
​
//Mybatis-plus的配置
@Component
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加数据权限插件
        interceptor.addInnerInterceptor(new DataPermissionInterceptor());
        return interceptor;
    }
}
测试
@SpringBootTest
@Slf4j
class DemoApplicationTests {
    @Autowired
    private UserController userController;
    @Test
    void contextLoads() {
        List<User> list = userController.list();
        log.info("查询结果:{}", list);
    }
}

输出:通过数据拦截器修改了SQL,所以人员只能看到本部门的数据

数据权限拦截器修改SQL: SELECT * FROM user WHERE department_id = 1
查询结果:[User(id=1, username=testuser1, password=123456, phone=13800138001, departmentId=1, createTime=2026-01-19T20:48:31), User(id=4, username=tech_user1, password=123456, phone=13800138004, departmentId=1, createTime=2026-01-19T20:48:31), User(id=5, username=tech_user2, password=123456, phone=13800138005, departmentId=1, createTime=2026-01-19T20:48:31)]
工作机制
  1. DAO方法被标注了@DataPermission注解时
  2. 从当前用户上下文获取部门ID(目前示例中是硬编码1)
  3. SELECT * FROM user 改为 SELECT * FROM user WHERE department_id = 1
  4. 只返回当前用户所在部门的数据

原理

Mybatis-Plus的插件机制是基于责任链模式和装饰器模式,是对Mybatis原生拦截器机制的增强封装

其插件的注册流程

Spring容器启动-> MybatisPlusConfig配置类->创建MybatisPlusInterceptor实例->调用addInnerInterceptor添加插件-> 插件被注册到内部列表(interceptors)中

注意:插件的执行顺序是,谁先注册到插件容器中,谁先执行

SQL执行的拦截流程

在描述拦截流程前,简单阐述下以下几个Mybatis底层的重要类

重要类

Mybatis底层允许拦截的类方法只有四个Executor、StatementHandler、ParameterHandler、ResultSetHandler。为啥时这四个,因为它们覆盖了SQL执行的全生命周期核心环节。

Executor调度执行StatementHandler 进行SQL构建,StatementHandler 设置SQL中占位符参数、ResultSetHandler封装执行SQL后返回的结果。所以我们只需要拦截以上类的方法,就能够拦截SQL执行流程了

SQL执行.png

Interceptor : 插件开发的接口,定义插件拦截逻辑和代理规则

InterceptorChain: 拦截器的【容量+责任链管理器】,负责管理所有拦截器并生成嵌套代理

Plugin: 动态代理的【生成器+处理器】,实现方法的拦截,插件的底层核心

流程解析
  1. 生成Mapper代理类:当调用我们文中 userDao.selectUserList 的时候,mybatis会通过JDK的动态代理生成MybatisMapperProxy代理类进行处理

  2. Mapper代理类执行: MybatisMapperProxy代理类根据方法类型执行对应的逻辑,通过SqlSession执行selectList方法

  3. 生成SqlSession执行,其调用过程:

    1. 在执行selectList方法前会生成SQLSessionProxy代理,代理会通过工厂类获取DefaultSqlSession实例
    2. DefaultSqlSession的会从配置数据源中获取连接,并通过Configuration类创建Executor实例
  4. Configuration类执行:interceptorChain.pluginAll方法,为生成当前动态代理对象,触发Plugin插件的plugin方法

  5. 循环嵌套代理(核心处理逻辑)InterceptorChain类内部维护插件容器,遍历所有注册在内的插件实例Interceptor,并调用插件中plugin方法为当前target(Executor)生成嵌套的动态代理。InterceptorChain是用责任链模式,核心逻辑:

        //InterceptorChain类
        public Object pluginAll(Object target) {
            Interceptor interceptor;
            for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
                interceptor = (Interceptor)var2.next(); //嵌套代理
            }
    ​
            return target;
        }
        
        //MybatisPlusInterceptor类
        public Object plugin(Object target) {
            return !(target instanceof Executor) && !(target instanceof StatementHandler) ? target : Plugin.wrap(target, this);
        }
        
        //原生Plugin代理类
        public static Object wrap(Object target, Interceptor interceptor) {
            // 1. 解析拦截器的 @Signature 注解,获取拦截规则
            Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
            // 2. 获取目标对象中匹配拦截规则的接口(必须是接口,JDK动态代理要求)
            Class<?> type = target.getClass();
            // 3. 如果有匹配的接口,生成动态代理对象;否则返回原对象(无需代理)
            Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
            return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
        }
    

    看到这会有同学想问什么是循环嵌套代理,循环嵌套代理

    循环: 检索所有注册到容器中的插件(如:MybatisPlusInterceptor或自定义插件类)

    嵌套:代码 interceptor = (Interceptor) var2.next() 而 Interceptor又执行Plugin.wrap() , 所以代码是 target = Plugin.wrap(target, interceptor)在循环的过程中一层套一层

    代理:通过JDK的动态代理对target进行代理

    所以使用循环嵌套代理是让所有的插件都可以拦截代理 target对象(Excutor)方法,使得在执行target对象方法时可以使用代理的所有注册的插件功能

    如果不使用循环嵌套代理,pluginAll方法的执行结果只会让最后一个插件代理生效

  6. SqlSessionInterceptor执行调用:method.invoke

  7. 在target目标对象被调用之前,通过JDK动态代理的方式会执行到plugin插件的intercept方法

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
                return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
            } catch (Exception var5) {
                throw ExceptionUtil.unwrapThrowable(var5);
            }
        }
    
  8. Plugin代理开始执行插件类的方法:

    1. MybatisMapperPlusInterceptor插件类内部维护了interceptors容器,遍历所有注册到容器的InnerInterceptor实例
    2. 调用容器中的实例:就是我们实现的DataPermissionInterceptor
    3. 执行 beforeQuery方法:添加数据权限
    4. 执行其他插件的逻辑
  9. 开始执行其他可以被拦截的方法: ParameterHandlerResultSetHanderStatementHandler

  10. 流程结束

总结

通过Mybatis-Plus的插件和Mybatis的拦截器机制,我们可以实现很多强大的功能,比如添加租户ID、分库分表、数据脱敏等等。