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