4、行权限核心概念:数据行级隔离、Owner机制、数据归属判断、行权限过滤器

行权限,说白了就是「谁能看哪几行数据」。

列权限管的是字段,行权限管的是记录。你想想看,一个公司里销售A和销售B,都能看到「订单表」这个对象,但A只能看自己的单子,B只能看自己的单子——这就是行级隔离。我见过不少团队在列权限上做得挺细,到了行权限就简单粗暴地拼SQL,结果数据漏了一地。

4.1 数据行级隔离:核心逻辑

行级隔离的本质,是给每一条数据打上一个「可见性标签」。系统在查询时,根据当前用户的身份,自动过滤掉不该看到的数据。

常见的隔离策略有三种:

策略 说明 典型场景
Owner隔离 用户只能看到自己创建或拥有的数据 个人工作台、待办事项
部门隔离 用户只能看到本部门及下级部门的数据 销售报表、工单系统
角色隔离 根据用户角色动态计算可见范围 管理者看全组、HR看全公司

我个人习惯把Owner隔离作为默认策略。为什么?因为它最简单、最不容易出错。你给每条数据记一个owner_id,查询时带上 WHERE owner_id = :currentUserId,完事。但实际项目中,往往不是这么单纯。

关键点:行级隔离不是「一刀切」的。同一个用户在不同模块、不同操作下,可见的数据范围可能完全不同。比如一个项目经理,在「任务列表」里只能看自己的任务,但在「项目概览」里能看到整个项目的所有任务。

4.2 Owner机制:谁拥有这条数据?

Owner机制是行权限的基石。每条数据必须有一个明确的「拥有者」。这个拥有者不一定是创建者,也可能是被分配者。

我在项目中遇到过一种情况:一个工单系统,工单的Owner是「当前处理人」。工单流转时,Owner会变。如果权限系统只认创建者,那流转后的工单就没人能看了。所以,Owner字段必须跟着业务状态走。

Owner字段的设计,我建议遵循以下原则:

  • 显式存储:在数据表中单独用一个字段存owner_id,不要靠计算得出
  • 支持多Owner:有些场景需要多个Owner(比如协同编辑),可以用owner_ids数组或关联表
  • Owner可继承:子数据的Owner默认继承父数据的Owner,除非显式指定

我的经验:Owner字段最好用UUID或业务主键,不要用自增ID。自增ID在分库分表、数据迁移时容易冲突,UUID全局唯一,省心很多。

4.3 数据归属判断:不只是查个字段

数据归属判断,就是判断「当前用户对这条数据有没有权限」。听起来简单,但实际判断逻辑往往比想象中复杂。

举个例子:一个报销单,创建者是员工A,审批者是经理B,财务是C。那么:

  • A能看到自己创建的报销单(Owner)
  • B能看到待自己审批的报销单(审批人)
  • C能看到所有待财务处理的报销单(角色)

你看,同一条数据,不同用户的归属判断逻辑完全不同。不能只靠一个owner_id字段搞定。

我常用的归属判断模型是这样的:

// 伪代码:数据归属判断
function canAccess(user, record) {
    // 1. 直接Owner
    if (record.ownerId === user.id) return true;

    // 2. 关联Owner(比如部门主管)
    if (record.deptId === user.deptId && user.isDeptManager) return true;

    // 3. 角色Owner(比如审批人、经办人)
    if (record.assigneeId === user.id) return true;

    // 4. 上级Owner(比如经理看下属的数据)
    if (user.subordinateIds.includes(record.ownerId)) return true;

    // 5. 兜底:管理员
    if (user.isAdmin) return true;

    return false;
}

注意:归属判断不能写死。不同业务模块的归属规则可能不同。我建议把归属判断逻辑做成可配置的策略,每个模块注册自己的判断器。否则,业务一变化,你就要改核心代码,风险极高。

4.4 行权限过滤器:在哪儿拦截?

行权限过滤器,是行权限落地的关键组件。它的职责是:在数据查询时,自动注入行权限条件,不让未授权的数据流出。

常见的拦截位置有三个:

拦截层 实现方式 优点 缺点
ORM层 在ORM查询构建时追加WHERE条件 对业务代码侵入小 复杂查询可能漏掉
数据库层 使用数据库行级安全策略(如PostgreSQL RLS) 性能好,强制生效 数据库迁移成本高
应用层 在Service层手动调用权限校验 灵活,可定制 容易遗漏,代码重复

我个人推荐「ORM层 + 应用层」双重过滤。ORM层做通用过滤,应用层做特殊场景的补充校验。我曾经在一个项目中只用了ORM层过滤,结果有个报表模块直接写了原生SQL,绕过了ORM,数据全漏了。从那以后,我坚持在关键查询上加一层应用层校验。

下面是一个基于MyBatis-Plus的行权限过滤器示例:

// 行权限过滤器(MyBatis-Plus 拦截器)
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class RowPermissionInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();

        // 获取当前用户
        User currentUser = SecurityContextHolder.getCurrentUser();

        // 只对需要行权限的表进行过滤
        if (sql.contains("FROM orders") && !currentUser.isAdmin()) {
            String condition = " AND orders.owner_id = " + currentUser.getId();
            sql = sql.replace("WHERE", "WHERE 1=1 " + condition);
            // 通过反射修改SQL
            Field field = boundSql.getClass().getDeclaredField("sql");
            field.setAccessible(true);
            field.set(boundSql, sql);
        }

        return invocation.proceed();
    }
}

避坑指南:我曾经在过滤器里直接拼接字符串,结果被SQL注入搞了一次。后来我改用参数化绑定,把owner_id作为参数传入,而不是拼在SQL里。记住:任何用户输入或上下文变量,都不要直接拼SQL。

4.5 本章知识体系

下面这张图,把行权限的核心概念串起来了。你可以看到,从数据行级隔离出发,经过Owner机制和归属判断,最终通过行权限过滤器落地。

行权限核心概念体系 数据行级隔离 Owner隔离 部门隔离 角色隔离 Owner机制 数据归属判断 行权限过滤器(ORM层 / 数据库层 / 应用层) 核心逻辑:数据打标签 → 用户身份匹配 → 动态注入过滤条件 → 安全返回结果

嗯,这张图你多看两眼。行权限的整个链路,就是从「数据怎么隔离」到「谁拥有数据」再到「怎么判断归属」,最后通过过滤器落地。每一步都有坑,但每一步也都有成熟的解法。