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

FastAPI — 类型就是契约

CORS 不是身份认证:设计原理

在 TT Lab 中继续学习

一句话总结

用真实的预检请求来验证源、方法和请求头的允许矩阵。

为什么需要它

前端读不到响应,于是对所有源都用通配符放行。在发送 Cookie 的请求中,策略变得更复杂,而开发者又误以为只要打开 CORS,外部请求就会被拦截。本实验把浏览器的读取策略和服务器认证分开。它不会替你实现认证功能。

工作原理

源(Origin)是 scheme、host、port 的组合。如果输入的配置里带有路径或凭据信息,就拒绝。对允许的源做精确比较,并且在允许凭据时禁止使用通配符。在 CORSMiddleware 中明确指定允许的方法和请求头。分别重现 OPTIONS 预检成功的情形,以及因为源、方法、请求头而失败的情形。

Origin + 요청 메서드 + 요청 헤더 → OPTIONS 정책 확인
허용: 출처/자격 헤더 제공 → 브라우저가 실제 요청
거절: 읽기 권한 없음 ≠ 서버 인증

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

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

1. 验证源的格式

origin(value) 在它是 http 或 https 的 URL,有 host,且没有 path、query、fragment 和用户信息时,返回输入字符串。其余都是 ValueError。末尾的 / 也属于 path,所以要拒绝。

判断依据:如果把整个 URL 都当作源允许,就可能把路径或用户信息混淆。

待评审的错误改动片段:

or url.query

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

2. 去除重复的源

origins(values) 先用 origin 验证每一项,再按首次出现的顺序去除重复,得到新列表。

判断依据:白名单是精确的源列表,而不是字符串的部分匹配。

待评审的错误改动片段:

[origin(value) for value in values]

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

3. 用白名单限制方法

methods(values) 只允许 GET、POST、PUT、DELETE、OPTIONS,并转成大写后去除重复。空列表或其他值都是 ValueError。

判断依据:不要悄悄加入没有被允许的 PATCH 和任意方法。

待评审的错误改动片段:

"DELETE","OPTIONS","PATCH"

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

4. 不同时允许凭据和通配符

policy(allowed, credentials) 检查 credentials 是否是 bool。如果 allowed 中有 '*',就是 ValueError,并返回 {allow_origins:origins(allowed), allow_credentials:credentials}。

判断依据:本实验的明确策略是,无论是否允许凭据,都不接受通配符。

待评审的错误改动片段:

not isinstance(credentials, (bool, int))

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

5. 挂上真实的 CORS 中间件

create_app(allowed, credentials=True) 是验证了 policy 并设置了 CORSMiddleware 的应用。它只允许 GET/POST,允许 Content-Type 和 X-Request-ID 请求头,并暴露(expose)X-Trace 响应头。GET /data 返回 {ok:True},以及 X-Trace='trace-1'。

判断依据:如果在 preflight 和实际响应中手动分别添加头,两套策略很容易产生偏差。

待评审的错误改动片段:

expose_headers=[]

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

6. 构造预检请求

preflight_headers(source, method, requested='X-Request-ID') 是一个带有 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 三个键的字典。method 是大写。

判断依据:实际的请求方法是 OPTIONS,要检查的方法在另一个头里。

待评审的错误改动片段:

method.lower()

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

7. 计算拒绝矩阵

preflight_status(app, source, method, requested='X-Request-ID') 用 TestClient 向 /data 发送 OPTIONS 请求,并返回 HTTP 状态。其他源、DELETE、X-Secret 头都必须是 400。

判断依据:不要把三种拒绝原因混在同一个请求里,才能找出缺失的策略。

待评审的错误改动片段:

client.get("/data",

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

8. 观察 CORS 与认证的区别

cors_observation(app, source) 发送 GET /data,并返回(状态,Access-Control-Allow-Origin 的值或 None,JSON 正文)。即使是不被允许的源,200 的正文也会被执行,但不应带有允许源的头。

判断依据:curl 或服务器之间的请求不遵守浏览器的 CORS 读取限制。

待评审的错误改动片段:

source

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

在现场相遇的样子

TestClient 不是浏览器。它检查 CORS 响应头和预检,但并不会实现浏览器自身的读取拦截。带着不被允许的 Origin 发送的普通 GET,也可能在服务器上被执行。敏感操作必须用单独的认证、权限和 CSRF 策略来保护。

下一项实验要做什么

八个步骤会连成一个可运行的成果。验证源的格式 → 去除重复的源 → 用白名单限制方法 → 不同时允许凭据和通配符 → 挂上真实的 CORS 中间件 → 构造预检请求 → 计算拒绝矩阵 → 观察 CORS 与认证的区别。

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