KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
00 · 全景地图:认证、授权、数据隔离是三件事 — keel 龙骨
权限相关的需求经常被混成一句"加个权限校验"。这一章先把它们拆开:认证回答"你是谁",授权回答"你能做什么",数据隔离回答"你能看到哪些行"。三者各用不同的机制,任何一层缺失都会形成真实漏洞。
权限相关的需求经常被混成一句"加个权限校验"。这一章先把它们拆开:认证回答"你是谁",授权回答"你能做什么",数据隔离回答"你能看到哪些行"。三者各用不同的机制,任何一层缺失都会形成真实漏洞。
一、现场:三个功能,各自被不同的机制卡住
同一个后台系统,三个月里出了三次权限问题:
- 用户反馈"改了权限还是看不到菜单"——授权层(角色与菜单的映射)没生效;
- 换成
curl直接打接口,用户拿到了本该看不到的用户列表——授权层只做了菜单、没做接口校验; - 一个跨租户的用户 id 被猜出来,接口返回了"资源不存在"——数据隔离层把租户条件写在了业务函数里,某个新接口忘了写。
三次问题的根因分别是:授权粒度不对、授权层次缺失、隔离机制依赖人自觉。
这一章要建立的直觉是:这三件事需要三层独立的机制,不能互相替代。
二、概念边界:三个词各管什么
| 概念 | 回答的问题 | 典型机制 | 出错时的表现 |
|---|---|---|---|
| 认证(Authentication) | 你是谁? | 登录、token、session | 401 Unauthorized |
| 授权(Authorization) | 你能做什么? | RBAC、权限点、ACL、注解 | 403 Forbidden |
| 数据隔离(Data Isolation) | 你能看到哪些行/哪些条? | 租户条件、数据范围、行级过滤 | 403,但更常见的是数据泄露(200 + 别人的数据) |
三个必须记住的不等式:
认证通过 ⇏ 有权限 登录成功不代表能调这个接口
有权限 ⇏ 看见全部数据 能调"列表接口"不代表能看见所有行
看见数据 ⇏ 能修改 可见级别与可编辑级别是分开的两件事(06 章)
第三个不等式是初学者最容易忽略的:很多系统把"能看"和"能改"合成一个开关,
结果要么用户改不了数据(业务投诉),要么用户能改不该改的(事故)。
三、一次完整运行:一次请求要穿过几道闸
配套项目用一张图把顺序固定下来(这张图是全课的骨架,后面每章都在往里填东西):
flowchart TD
REQ["请求进来"] --> AUTH["① 认证:有没有有效凭证?<br/>没有 → 401"]
AUTH --> TEN["② 租户上下文:这个人在哪个租户里?<br/>不属于 → 403"]
TEN --> PERM["③ 功能权限:有没有这个权限点?<br/>没有 → 403"]
PERM --> ROW["④ 数据范围:能看到哪些行?<br/>追加 WHERE 条件"]
ROW --> OBJ["⑤ 资源级别:这一条数据我能动到什么程度?<br/>不够 → 403"]
OBJ --> BIZ["业务逻辑"]
BIZ --> AUDIT["⑥ 审计:谁、在什么时候、对哪条数据做了什么"]
顺序不是随意的,它有两个硬约束:
- 认证必须在最前。没确定"你是谁"就没法判断"你能做什么"——权限判定的主体都不存在。
- 租户上下文必须在功能权限之前。权限点本身是租户内定义的(角色是租户内自定义的),不知道在哪个租户,无从判断有没有这个角色。
第 ④ 步和第 ⑤ 步的顺序则取决于业务:列表接口走 ④,详情/编辑接口走 ⑤,
两者都需要时先窄后宽(先定位到行,再判断能不能动它)。
四、模型谱系:为什么最后是 RBAC + ACL 的组合
权限模型的历史演进,是在"表达力"和"管理成本"之间反复权衡的结果:
| 模型 | 规则形态 | 优点 | 致命短板 |
|---|---|---|---|
| ACL(访问控制列表) | 逐条资源显式授权 | 表达力最强,任意资源任意主体 | 条目数 = 资源数 × 主体数,无法维护 |
| RBAC(基于角色) | 主体 → 角色 → 权限 | 角色复用,管理成本低 | 只能表达"这个岗位能做什么",表达不了"这条数据归谁" |
| 数据范围 / 行级 | 在查询上追加可见性条件 | 解决"看到哪些行" | 与 RBAC 是两套独立机制,容易只做一套 |
| ABAC(基于属性) | 用规则表达式判定 | 表达力强、可动态 | 规则引擎难调试、难审计 |
国内后台系统(若依及其同类)几乎都是同一个组合拳:
RBAC 管「能不能调这个接口」
+ 菜单表管「左侧看得见什么」
+ 数据范围管「列表看到哪些行」
+ 少数敏感场景再加资源级 ACL
这不是因为它最优,而是因为它在可管理性上赢了:任何一个新同事入职,
只需要给他一个角色,而不需要给他成百上千条资源授权。
配套项目的演示数据规模已经能看出这个代价(project/seed.py):
tenant 2 行
member 4 行
role 4 行
permission 5 行
menu 3 行
resource 3 行
resource_acl 4 行
4 个角色管住了全部功能权限,而资源授权是按条维护的——这正是 06 章要展开的张力。
五、失败注入:把三层分别打掉
用配套项目做三个实验,每个只打掉一层:
实验 A:只留认证,去掉功能权限(把 needs("system:user:delete") 从路由依赖里删掉)
python api_probe.py # 观察"运营角色删用户"这一行从 403 变成 200
实验 B:只留功能权限,去掉数据范围(把 visible_org_ids 恒返回 None)
u_alice scope=all orgs=None 可见用户数=3
u_bob scope=self orgs=None 可见用户数=3 ← Bob 不该看见 Carol 和 Alice
实验 C:数据隔离层漏一个查询(把某接口的 tenant_id 条件去掉)
跨租户读资源 GET /resources/r_flow_y -> 200 ← 换租户后仍能读别人的资源
三个实验的共同点:功能都还"能跑",没有任何报错。这就是权限 bug 的可怕之处——
它不会在测试里失败,只会在真实用户手里出事。
六、主动破坏清单(每一条都要亲手跑一遍)
- 猜 id:把 URL 里的资源 id 换成别人的,看返回什么(应该 404,不是 403,也不是 200);
- 改租户头:把
X-Tenant-Id换成另一个租户,看是否被拒; - 跨层调用:拿到一个低权限角色的 token,直接请求管理员接口;
- 改权限不刷新:在数据库里给角色加一个权限点,不重新登录,看接口是否立刻生效(07 章主题);
- 数据范围调换:把角色的
data_scope从org改成self,看列表是否变少。
七、生产环境怎样替换
| 教学替身 | 生产替换 | 要点 |
|---|---|---|
请求头里的 X-User-Id |
真实会话/JWT 校验后的用户标识 | 配套项目用请求头是为了让例子自包含,生产绝不能信客户端自称的身份 |
| SQLite 单连接 | 连接池 + 事务 | 权限判定要读多张表,注意一致性 |
| 硬编码角色码 | 角色数据落库 | 角色是租户内数据,不要写进代码 |
| 无审计 | 授权变更、敏感操作审计日志 | 权限系统自身要可审计(07 章) |
八、练习与验收
练习 1:给你手上的项目画一张"请求穿过几道闸"的图,标出每道闸对应的代码位置;找出其中依赖人自觉(而不是机制保证)的地方。
练习 2:跑实验 A/B/C,记录每个漏洞的表现形式(状态码、数据内容),并说明为什么它们都不会被普通功能测试发现。
验收点:不看资料,说出认证/授权/数据隔离分别回答什么问题、各自的失效表现;解释为什么"菜单可见"不能替代"接口可调";说出 RBAC 与 ACL 各自的管理成本与短板。
现在能解释什么:三层权限各管什么、为什么不能互相替代;一次请求穿过六道闸的顺序及其硬约束;RBAC/ACL/数据范围的分工与代价。
下一步:第 01 章把最基础的那层(若依式权限)从零手搓一遍——表结构、权限点、三层授权。