RuoYi-Cloud-Plus 数据权限实现原理解析

RuoYi-Cloud-Plus 数据权限实现原理解析

什么是数据权限?

数据权限是控制用户能够访问哪些数据的权限机制。在实际业务场景中,我们经常遇到这样的需求:

  • 普通员工只能查看自己创建的数据
  • 部门经理可以查看本部门所有员工的数据
  • 总经理可以查看全公司的数据

这种按照用户角色和组织结构控制数据访问范围的机制就是数据权限控制。

RuoYi-Cloud-Plus 数据权限设计思路

RuoYi-Cloud-Plus 采用了 AOP(面向切面编程)+ MyBatis 拦截器的组合方式来实现数据权限控制:

  1. 通过自定义注解标记需要进行数据权限控制的方法
  2. 利用 AOP 在方法执行前设置权限上下文
  3. 通过 MyBatis 拦截器拦截 SQL 语句并动态添加过滤条件

核心组件解析

1. 数据权限注解

RuoYi-Cloud-Plus 定义了两个核心注解:

@DataPermission 注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
    DataColumn[] value();     // 数据权限配置数组
    String joinStr() default ""; // 权限拼接标识符
}

@DataPermission 注解用于标记需要进行数据权限控制的方法或类,其中:

  • value:包含一个或多个 @DataColumn 注解,定义数据权限的详细配置
  • joinStr:指定多个数据权限条件之间的连接方式(AND 或 OR)

@DataColumn 注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataColumn {
    String[] key() default "deptName";    // 占位符关键字
    String[] value() default "dept_id";   // 占位符替换值
    String permission() default "";       // 权限标识符
}

@DataColumn 注解用于定义数据权限的占位符和替换值:

  • key:SQL 模板中的占位符关键字
  • value:实际数据库表中的字段名
  • permission:权限标识符,拥有此权限的用户将不受数据权限限制

2. AOP 切面处理

AOP 切面由三个核心组件构成:

DataPermissionPointcut - 切点定义

public class DataPermissionPointcut extends StaticMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        // 匹配带有 @DataPermission 注解的方法
        if (method.isAnnotationPresent(DataPermission.class)) {
            return true;
        }
        
        // 处理 JDK 动态代理的情况
        Class<?> targetClassRef = targetClass;
        if (Proxy.isProxyClass(targetClassRef)) {
            targetClassRef = targetClass.getInterfaces()[0];
        }
        return targetClassRef.isAnnotationPresent(DataPermission.class);
    }
}

DataPermissionAdvice - 通知逻辑

public class DataPermissionAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object target = invocation.getThis();
        Method method = invocation.getMethod();
        Object[] args = invocation.getArguments();
        
        // 设置权限注解到上下文
        DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method, args));
        try {
            // 执行目标方法
            return invocation.proceed();
        } finally {
            // 清除权限注解
            DataPermissionHelper.removePermission();
        }
    }
}

DataPermissionPointcutAdvisor - 切面组合

public class DataPermissionPointcutAdvisor extends AbstractPointcutAdvisor {
    private final Advice advice;
    private final Pointcut pointcut;

    public DataPermissionPointcutAdvisor() {
        this.advice = new DataPermissionAdvice();
        this.pointcut = new DataPermissionPointcut();
    }

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    @Override
    public Advice getAdvice() {
        return this.advice;
    }
}

3. 权限上下文管理

DataPermissionHelper 负责管理权限上下文,使用 ThreadLocal 存储当前线程的权限信息:

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DataPermissionHelper {
    // 使用 ThreadLocal 存储当前线程的权限注解
    private static final ThreadLocal<DataPermission> PERMISSION_CACHE = new ThreadLocal<>();
    
    // 设置权限注解
    public static void setPermission(DataPermission dataPermission) {
        PERMISSION_CACHE.set(dataPermission);
    }
    
    // 获取权限注解
    public static DataPermission getPermission() {
        return PERMISSION_CACHE.get();
    }
    
    // 清除权限注解
    public static void removePermission() {
        PERMISSION_CACHE.remove();
    }
}

4. MyBatis 拦截器

MyBatis 拦截器负责在 SQL 执行前动态添加数据权限过滤条件:

public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor {
    private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler();

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, 
                           RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 检查是否需要忽略数据权限处理
        if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
            return;
        }
        
        // 检查是否缺少有效的数据权限注解
        if (dataPermissionHandler.invalid()) {
            return;
        }
        
        // 解析并修改 SQL
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
    }
}

数据权限处理流程

1. 方法调用阶段

当执行带有 @DataPermission 注解的方法时:

  1. AOP 拦截器捕获方法调用
  2. 从方法或类上获取 @DataPermission 注解信息
  3. 将注解信息存储到 ThreadLocal
  4. 执行目标方法(通常是 MyBatis Mapper 方法)
  5. 方法执行完成后,清除 ThreadLocal 中的注解信息

2. SQL 拦截阶段

当 MyBatis 执行 SQL 语句时:

  1. MyBatis 拦截器捕获 SQL 执行请求
  2. ThreadLocal 中获取权限注解信息
  3. 根据当前用户角色和权限范围构建过滤条件
  4. 将过滤条件动态添加到 SQL 中

3. 权限条件构建

权限条件构建过程如下:

  1. 获取当前登录用户信息
  2. 判断用户是否为超级管理员(超级管理员不受数据权限限制)
  3. 根据用户角色获取数据范围类型
  4. 使用 SpEL 表达式解析权限模板
  5. 替换占位符生成最终的 SQL 过滤条件

实际使用示例

基本使用

// 在 Mapper 接口上使用数据权限注解
@DataPermission({
    @DataColumn(key = "deptName", value = "dept_id"),
    @DataColumn(key = "userName", value = "user_id")
})
public interface SysUserMapper {
    List<SysUser> selectUserList(SysUser user);
}

假设当前用户是部门经理,属于部门 ID 为 1、2、3 的部门,用户 ID 为 1001,生成的 SQL 可能类似于:

SELECT * FROM sys_user WHERE (dept_id IN (1, 2, 3) OR user_id = 1001)

使用 joinStr 控制连接方式

@DataPermission(value = {
    @DataColumn(key = "deptName", value = "dept_id"),
    @DataColumn(key = "userName", value = "user_id")
}, joinStr = "AND")
public interface SysUserMapper {
    List<SysUser> selectUserList(SysUser user);
}

使用 joinStr = "AND" 后,生成的 SQL 可能类似于:

SELECT * FROM sys_user WHERE (dept_id IN (1, 2, 3) AND user_id = 1001)

使用权限标识符

@DataPermission({
    @DataColumn(key = "deptName", value = "dept_id"),
    @DataColumn(key = "userName", value = "user_id", permission = "system:user:all")
})
public interface SysUserMapper {
    List<SysUser> selectUserList(SysUser user);
}

如果用户拥有 "system:user:all" 权限,则不受数据权限限制,生成的 SQL 不会添加额外的过滤条件。

joinStr 参数详解

在 @DataPermission 注解中,joinStr 属性用于指定多个数据权限条件之间的连接方式:

  • 不指定 joinStr:查询操作默认使用 "OR",更新/删除操作默认使用 "AND"
  • 指定 joinStr = "AND":多个条件使用 AND 连接
  • 指定 joinStr = "OR":多个条件使用 OR 连接

总结

RuoYi-Cloud-Plus 的数据权限实现具有以下优势:

  1. 无侵入性:通过注解方式实现,业务代码无需修改
  2. 灵活性:支持在方法和类级别使用,可以定义多个数据列的过滤规则
  3. 可扩展性:通过 SpEL 表达式支持复杂的权限逻辑
  4. 高性能:使用 ThreadLocal 存储上下文信息,避免重复查询
posted @ 2025-10-25 21:40  WonderC  阅读(14)  评论(0)    收藏  举报