题干与适用场景
页面运行在 https://app.example.com,API 位于 https://api.example.com。前端需要提交用户偏好:
await fetch("https://api.example.com/profile/preferences", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify({ compactMode: true }),
})服务端当前给 OPTIONS 返回:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POSTcurl 直接调用 POST 可以成功,浏览器控制台却只显示跨源错误。请说明:
- 为什么这两个子域仍是跨源请求;
- 浏览器为什么先发 OPTIONS,什么情况下才会继续发 POST;
- 当前响应头有哪些问题,应该如何按最小权限修复;
- CORS、Cookie、认证、授权和 CSRF 分别负责什么;
- 如何在开发者工具和自动化测试中证明修复有效。
这道题适用于前端和全栈岗位。它考察的不是背一组响应头,而是能否从浏览器网络阶段反推 故障位置,并把“请求是否发送”“凭证是否携带”“脚本能否读取响应”“业务是否允许操作” 四件事分开。
面试官考察点
首先要准确判断源。Origin 由协议、主机和端口组成。两个地址虽然同为 HTTPS,且通常属于 同一站点,但主机不同,因此是同站跨源请求。CORS 按源判断;Cookie 的 SameSite 按站点 判断,两者不能混用。
其次要理解预检条件。application/json 不属于 CORS-safelisted 请求头值, X-CSRF-Token 也不是 safelisted 请求头,因此浏览器先发送 OPTIONS,询问服务端是否允许 实际方法和请求头。预检失败时,实际 POST 不会发送。
第三,要能读出当前配置的两个关键错误:带凭证的响应不能使用 Access-Control-Allow-Origin: *;预检也没有允许 Content-Type 与 X-CSRF-Token。允许方法中出现 POST 还不够。
最后要守住安全边界。CORS 决定浏览器是否把跨源响应共享给脚本,不替代认证、授权或 CSRF 防护。服务端不能为了消除控制台错误就反射任意 Origin,也不应把所有方法和请求头全部放开。
回答前需要澄清的问题
- 失败发生在哪个网络阶段? Network 面板里完全没有请求、只有 OPTIONS、已有 POST 但
JavaScript 读不到响应,代表不同故障。
- 请求是否必须带 Cookie? 若使用 Bearer token 或匿名公共资源,凭证规则与允许源策略会
不同。
- 允许哪些精确的前端 Origin? 需要列出生产、预发布和本地开发源,不能只说“允许公司
域名”。协议和端口也必须精确。
- OPTIONS 是否被网关、认证中间件或重定向拦截? 预检通常不带用户凭证,不能先要求它
通过登录态认证。
- Cookie 的 Domain、Path、Secure 与 SameSite 是什么? CORS 通过也不保证浏览器会发送
Cookie。
- 实际响应和错误响应是否都带 CORS 头? 若 401、403 或 500 丢失允许源头,浏览器可能
用泛化的 CORS 错误遮住真正状态。
- 是否存在 CDN 或共享缓存? 如果响应根据 Origin 动态变化,应发送
Vary: Origin,避免
一个源的响应被错误复用给另一个源。
30 秒回答框架
“我会把问题拆成四道关卡:源判断、预检许可、凭证传输和响应共享。两个子域主机不同,所以 是跨源。JSON Content-Type 和 X-CSRF-Token 会触发 OPTIONS;浏览器只有看到精确允许的 Origin、POST 和两个请求头,才会发送实际请求。这里使用了 credentials: include,所以 Allow-Origin 不能是星号,应该在严格白名单命中后返回 https://app.example.com,同时返回 Allow-Credentials: true 和 Vary: Origin。OPTIONS 本身不依赖登录 Cookie;真正的 POST 仍要做认证、授权和 CSRF 校验。最后我会在 Network 面板逐个核对 OPTIONS、POST、Cookie、响应头和业务状态,并用允许源与恶意源的矩阵验证。”
分步骤深入解答
第一步:先画出浏览器实际执行的状态机。
预检请求大致如下:
OPTIONS /profile/preferences HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-csrf-token浏览器比较的不是“前后端是不是一家公司的”,而是响应是否明确许可这个 Origin、方法和请求 头。可以用下表快速定位:
| Network 证据 | 说明 | 下一步 | |---|---|---| | 没有 OPTIONS 或 POST | JavaScript 尚未执行、URL 被 CSP 等更早规则拦截 | 先检查调用与控制台 | | 只有失败的 OPTIONS | 实际 POST 未发送 | 检查预检状态码、重定向和允许头 | | OPTIONS 成功,POST 无 Cookie | CORS 许可已过,凭证链路有问题 | 检查 credentials 与 Cookie 属性 | | POST 返回 401/403/500,但脚本只见 CORS | 错误响应缺少有效 CORS 头 | 保留真实状态并补齐允许源头 | | POST 成功且脚本可读 | CORS 链路通过 | 再验证业务状态与安全负例 |
第二步:按请求推导最小允许集合。
对于题设中唯一允许的生产前端,可以返回:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, X-CSRF-Token
Vary: Origin服务端应先把 Origin 与精确允许列表比较,命中后才写回该值。不要把客户端传来的 Origin 无条件复制到响应。若还有预发布或本地环境,应把每个完整 Origin 分别列入相应环境配置, 不要用字符串后缀匹配来接受伪造的相似域名。
公开、匿名且不带凭证的资源有时可以使用 *。这个用户偏好接口带 Cookie,属于敏感操作, 必须返回具体 Origin。预检 OPTIONS 不携带 Cookie;网关可以在鉴权前处理预检,但仍要严格 校验 Origin、方法和请求头。
第三步:再检查实际请求与凭证。
预检成功后,浏览器才会发送 POST。credentials: "include" 允许跨源请求进入凭证处理, 但 Cookie 是否真正附带仍受 Domain、Path、Secure、SameSite 和浏览器隐私策略约束。
本例两个 HTTPS 子域通常是同站跨源:SameSite 规则可能允许 Cookie,CORS 仍然适用。不能从 “CORS 已通过”推导“Cookie 一定发送”,也不能从“Cookie 已发送”推导“脚本可以读取响应”。 应在 Network 面板分别检查 Request Cookies、响应状态和 CORS 响应头。
第四步:把 CORS 与业务安全拆开。
CORS 是浏览器的响应共享协议,不是服务端访问控制。curl 和服务器端 HTTP 客户端不会替浏览器 执行同源策略,所以 curl 成功只能证明 API 能处理请求,不能证明浏览器链路正确。
实际 POST 仍需:
- 验证登录身份;
- 验证当前用户是否有权修改目标资源;
- 验证 CSRF token 或采用同等强度的防护;
- 校验输入并保持幂等或重复提交策略;
- 对允许源的 401、403 和 500 响应同样返回正确 CORS 头,让前端看见真实错误。
“恶意页面读不到响应”不等于“请求没有副作用”。某些不需要预检的跨源表单式请求可能已经 到达服务端,因此敏感写操作不能只靠 CORS 防 CSRF。
第五步:用正负测试证明边界。
修复后至少验证:
| 用例 | 预期结果 | |---|---| | 允许源发送题设请求 | OPTIONS 204,随后 POST;脚本读到真实响应 | | 允许源缺少 CSRF token | 依协议配置决定是否预检;实际写入必须被业务校验拒绝 | | 未允许源发送相同请求 | 不返回允许该源的 CORS 头,实际预检不通过 | | 请求未允许的方法或请求头 | 预检失败,POST 不发送,状态不变化 | | Cookie 过期 | POST 返回真实 401,前端可读取并进入登录流程 | | 服务端返回 500 | 前端看到 500 与结构化错误,而非泛化 CORS 提示 | | 连续相同请求命中预检缓存 | 行为仍正确;配置变更时重新验证冷缓存 |
同时核对服务端日志:失败的预检不应出现对应 POST,也不应产生用户偏好更新。日志可以记录 Origin、方法、请求头集合和拒绝原因,不应记录 Cookie 或 CSRF token 值。
高质量示范回答
“我先判断请求所处阶段。app.example.com 和 api.example.com 的主机不同,所以即使它们 同站,仍然跨源。这个 POST 使用 JSON Content-Type 和 X-CSRF-Token,浏览器会先发一个不带 Cookie 的 OPTIONS,其中声明 Origin、POST 和两个非简单请求头。只有预检响应允许这三项, 浏览器才继续发送 POST。
当前配置有两个直接问题。第一,前端设置了 credentials: include,带凭证响应不能把 Access-Control-Allow-Origin 设为星号;第二,响应没有 Access-Control-Allow-Headers: Content-Type, X-CSRF-Token。我会在服务端用精确白名单校验 Origin,命中 https://app.example.com 后返回这个具体值、Allow-Credentials、POST、两个 允许请求头以及 Vary: Origin。OPTIONS 放在用户鉴权中间件之前处理,但未允许的 Origin、 方法或请求头仍会被拒绝。
预检通过后,我再检查 POST 的 Request Cookies。credentials: include 只是必要条件,Cookie 仍受 Domain、Path、Secure、SameSite 和浏览器策略约束。真正的 POST 必须继续做认证、授权和 CSRF 校验,因为 CORS 只控制浏览器是否把响应共享给脚本,不是访问控制。
验证时我会在 Network 面板确认 OPTIONS 成功后才出现 POST,检查请求 Cookie、实际状态与 响应头;再用允许源、未允许源、未允许方法、过期登录态和 500 响应做正负测试。服务端日志还 要证明预检失败时没有 POST,也没有状态变化。curl 成功只能作为 API 基线,不能替代浏览器 CORS 验证。”
常见错误
- 把同站当成同源 → 子域不同仍会触发 CORS → 按协议、主机、端口逐项比较。
- 看到 POST 在允许列表就认为预检会过 → 非简单请求头未被允许 → **同时核对 Origin、
Method 和 Headers。**
- 带凭证时使用
Allow-Origin:→ 浏览器拒绝把响应暴露给脚本 → *返回白名单命中的
具体 Origin。**
- 无条件回显请求 Origin → 任意网站都可能获得带凭证响应的读取权限 → **先做精确允许
列表校验。**
- 要求 OPTIONS 先通过用户登录 → 预检通常不携带凭证 → 在鉴权前完成受限的预检处理。
- 用
mode: "no-cors"修复 → 前端拿到的是不可读的 opaque response → **修正服务端
CORS 协议。**
- 把 CORS 当作 CSRF 防护 → 请求可能到达服务端并产生副作用 → **敏感写操作继续校验
CSRF 与授权。**
- 只给 2xx 加 CORS 头 → 401 或 500 被伪装成跨源错误 → **允许源的错误响应也返回一致
CORS 头。**
- 只用 curl 验证 → curl 不执行浏览器同源策略 → 必须用真实浏览器网络证据验收。
追问及应对
为什么请求有时不发 OPTIONS,却仍然报 CORS?
满足 safelisted 方法、请求头与 Content-Type 的请求可以直接发送。浏览器仍会检查实际响应的 CORS 头;不满足共享条件时,JavaScript 读不到响应。此时服务端可能已经处理请求,所以要从 Network 面板确认“请求已发送但响应未共享”,不能把所有 CORS 错误都叫预检失败。
为什么 Access-Control-Allow-Credentials 不应该出现在预检请求里?
它是服务端的响应头,表示浏览器可以向脚本暴露凭证模式下的实际响应。客户端通过 Fetch 的 credentials mode 表达凭证意图;预检请求本身不带用户凭证。实际响应也要满足具体允许源和 Allow-Credentials 条件。
Vary: Origin 解决什么问题?
当服务端根据请求 Origin 返回不同的 Access-Control-Allow-Origin 时,共享缓存需要把 Origin 纳入缓存键。否则,一个允许源的响应可能被复用给另一个源,造成错误放行或错误拦截。
CORS 已正确,为什么 Cookie 仍然没有发送?
继续检查前端是否设置 credentials: "include",以及 Cookie 的 Domain、Path、Secure、 SameSite、过期时间和浏览器隐私限制。CORS 与 Cookie 传输是相邻但独立的关卡。
如何处理多个合法前端域名?
维护精确允许列表,请求到达时校验完整 Origin,命中后回写该 Origin 并发送 Vary: Origin。 不要返回多个 Allow-Origin 值,也不要用宽松正则、后缀匹配或无条件反射。
前端能否自行修复服务端 CORS?
不能。前端可以减少不必要的非简单请求头,或按协议设置 credentials,但允许跨源共享的响应头 必须由 API、反向代理或网关正确返回。生产代码不应依赖浏览器插件或关闭安全策略。
应该把预检缓存多久?
根据配置变更频率、撤销需求和浏览器上限选择,并在发布 CORS 变更时验证冷缓存与已有缓存两种 路径。缓存能减少 OPTIONS 流量,但会延长错误许可的生效时间,不能用来掩盖配置问题。