TT Lab
开始
学习 学习路径 课程

FastAPI — 类型就是契约

有效令牌不代表拥有文档访问权:设计原理

在 TT Lab 中继续学习

一句话总结

把认证、权限和所有权分开,并用同样的 404 来隐藏资源是否存在。

为什么需要它

已登录的用户只是改了地址里的文档编号,就读到了别的团队的文档。令牌有效这件事,与有权读取某份特定文档,是两回事。本实验用固定的令牌字典来代替认证服务器。它不是实现签发令牌或 JWT 签名的实验,而是实现认证结果之后的权限边界的实验。

工作原理

请求按照 Bearer 头格式检查、令牌查询、scope 确认、所有者确认的顺序进行。没有认证信息或认证信息有误,返回 401,并发送 WWW-Authenticate 头。身份已确认但没有 read 权限,返回 403。有读取权限的用户请求不存在的文档或别人的文档,两者都是 404。公开响应中只保留 id 和 title,以免泄露内部所有者和成本。

헤더 → 인증 401 → scope 403 → 소유권/존재 404 → 공개 필드 200

阅读契约并预测失败的工作表

下面不是让你把整个实现背下来的答案,而是分步骤的代码评审。每个改动片段都故意破坏了契约。请注意,改动之后,正常用例仍然可能通过。在运行之前,先预测观测哪些输入、异常和状态才能看出差异,实现之后,再把这个预测与结果进行比较。

1. 分离 Bearer 头

bearer(header) 在头部恰好以 'Bearer ' 开头,且后面有一个不含空格的令牌时,返回该令牌。None、空令牌、其他 scheme、多余的空格都是 ValueError。

判断依据:如果把头部随意切成多段,就可能把空格错误当成正常令牌接受下来。

待评审的错误改动片段:

header[6:]

请与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观测对象。

2. 复制身份后再返回

principal(token, users) 返回令牌字典中用户的 {id, scopes},但要把 scopes 列表也一并复制。不认识的令牌是 ValueError。

判断依据:如果修改返回的 scopes 会连带改变原用户的权限,不同请求之间的权限就会混在一起。

待评审的错误改动片段:

user["scopes"]

请与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观测对象。

3. 精确比较权限

require_scope(user, scope) 在 scopes 中恰好含有 scope 字符串时返回 None,没有时抛出 PermissionError。read-all 不是 read。

判断依据:子串比较会把更长的权限名称误认为另一种权限。

待评审的错误改动片段:

if False:

请与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观测对象。

4. 单独确认所有权

visible(user, document) 只有在 document 不是 None,并且 owner 与 user 的 id 完全相同时才返回 True。

判断依据:把资源不存在和他人所有合并成同一个判定。

待评审的错误改动片段:

True

请与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观测对象。

5. 用白名单挑选响应字段

public_document(document) 是只含 id 和 title 的新字典。不包含 owner 或 internal_cost。

判断依据:不要从原件中删除字段,而要组装新的响应。

待评审的错误改动片段:

("id", "title", "owner")

请与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观测对象。

6. 让错误符合 HTTP 契约

authenticate(header, users) 把 bearer 和 principal 连接起来。ValueError 要变成 HTTPException(401),并且 headers 中 WWW-Authenticate 的值是 Bearer。

判断依据:不要把认证失败和应用错误统统归成一个 500。

待评审的错误改动片段:

HTTPException(403,

请与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观测对象。

7. 固定拒绝的顺序

read_document(user, documents, document_id) 在没有 read scope 时是 HTTPException(403),文档不存在或属于别人时是 HTTPException(404),否则就是 public_document 的结果。

判断依据:即使认证之后,scope 和所有权也必须分别确认。

待评审的错误改动片段:

HTTPException(403, "not found")

请与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观测对象。

8. 在真实请求中封住边界

create_app(users, documents) 返回一个 FastAPI 应用,它在 GET /documents/{document_id} 中接收 Authorization 头,并调用 authenticate 和 read_document。请用真实请求验证 200、401、403、404 以及非公开字段是否被去除。

判断依据:即使函数各自都对,如果在路径中漏掉了调用,访问控制就不会生效。

待评审的错误改动片段:

authorization: str | None = None

请与包含该片段的函数的公开契约对照。如果仅凭一个成功用例无法区分,就把应被拒绝的输入或失败之后的状态选作观测对象。

在现场相遇的样子

固定的令牌字典是教学用的输入。生产环境的认证,还需要过期、签名、撤销和安全保管。仅仅让 404 保持一致,也并不能消除通过响应时间或访问日志做出的所有推断。还要一并检查被拒绝的请求有没有连原始数据也改掉。

下一项实验要做什么

八个步骤会连成一个可运行的成果。分离 Bearer 头 → 复制身份后再返回 → 精确比较权限 → 单独确认所有权 → 用白名单挑选响应字段 → 让错误符合 HTTP 契约 → 固定拒绝的顺序 → 在真实请求中封住边界。

每一步检查的不是函数或文件是否存在,而是实际的返回值、异常和状态变化。看过正确答案之后,请故意改动边界比较或清理代码,确认哪些测试会失败。请说明前面的测试为什么在下一步中依然保持有效,并写出一条本实验不能保证的生产条件。