CORS 不是身份认证:设计原理
一句话总结
用真实的预检请求来验证源、方法和请求头的允许矩阵。
为什么需要它
前端读不到响应,于是对所有源都用通配符放行。在发送 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 与认证的区别。
每一步检查的不是函数或文件是否存在,而是实际的返回值、异常和状态变化。看过正确答案之后,请故意改动边界比较或清理代码,确认哪些测试会失败。请说明前面的测试为什么在下一步中依然保持有效,并写出一条本实验不能保证的生产条件。