KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

00 · 全景地图:认证、授权、数据隔离是三件事 — keel 龙骨

权限相关的需求经常被混成一句"加个权限校验"。这一章先把它们拆开:认证回答"你是谁",授权回答"你能做什么",数据隔离回答"你能看到哪些行"。三者各用不同的机制,任何一层缺失都会形成真实漏洞。

权限相关的需求经常被混成一句"加个权限校验"。这一章先把它们拆开:认证回答"你是谁",授权回答"你能做什么",数据隔离回答"你能看到哪些行"。三者各用不同的机制,任何一层缺失都会形成真实漏洞。


一、现场:三个功能,各自被不同的机制卡住

同一个后台系统,三个月里出了三次权限问题:

  1. 用户反馈"改了权限还是看不到菜单"——授权层(角色与菜单的映射)没生效;
  2. 换成 curl 直接打接口,用户拿到了本该看不到的用户列表——授权层只做了菜单、没做接口校验;
  3. 一个跨租户的用户 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["⑥ 审计:谁、在什么时候、对哪条数据做了什么"]

顺序不是随意的,它有两个硬约束:

  1. 认证必须在最前。没确定"你是谁"就没法判断"你能做什么"——权限判定的主体都不存在。
  2. 租户上下文必须在功能权限之前。权限点本身是租户内定义的(角色是租户内自定义的),不知道在哪个租户,无从判断有没有这个角色。

第 ④ 步和第 ⑤ 步的顺序则取决于业务:列表接口走 ④,详情/编辑接口走 ⑤,
两者都需要时先窄后宽(先定位到行,再判断能不能动它)。

四、模型谱系:为什么最后是 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 的可怕之处——
它不会在测试里失败,只会在真实用户手里出事。

六、主动破坏清单(每一条都要亲手跑一遍)

  1. 猜 id:把 URL 里的资源 id 换成别人的,看返回什么(应该 404,不是 403,也不是 200);
  2. 改租户头:把 X-Tenant-Id 换成另一个租户,看是否被拒;
  3. 跨层调用:拿到一个低权限角色的 token,直接请求管理员接口;
  4. 改权限不刷新:在数据库里给角色加一个权限点,不重新登录,看接口是否立刻生效(07 章主题);
  5. 数据范围调换:把角色的 data_scope 从 org 改成 self,看列表是否变少。

七、生产环境怎样替换

教学替身 生产替换 要点
请求头里的 X-User-Id 真实会话/JWT 校验后的用户标识 配套项目用请求头是为了让例子自包含,生产绝不能信客户端自称的身份
SQLite 单连接 连接池 + 事务 权限判定要读多张表,注意一致性
硬编码角色码 角色数据落库 角色是租户内数据,不要写进代码
无审计 授权变更、敏感操作审计日志 权限系统自身要可审计(07 章)

八、练习与验收

练习 1:给你手上的项目画一张"请求穿过几道闸"的图,标出每道闸对应的代码位置;找出其中依赖人自觉(而不是机制保证)的地方。

练习 2:跑实验 A/B/C,记录每个漏洞的表现形式(状态码、数据内容),并说明为什么它们都不会被普通功能测试发现。

验收点:不看资料,说出认证/授权/数据隔离分别回答什么问题、各自的失效表现;解释为什么"菜单可见"不能替代"接口可调";说出 RBAC 与 ACL 各自的管理成本与短板。


现在能解释什么:三层权限各管什么、为什么不能互相替代;一次请求穿过六道闸的顺序及其硬约束;RBAC/ACL/数据范围的分工与代价。

下一步:第 01 章把最基础的那层(若依式权限)从零手搓一遍——表结构、权限点、三层授权。

进入 keel 阅读