← 返回文章列表

CORS 到底在拦什么:同源与预检

HTTP安全入门

同源策略是什么

协议、域名、端口三者全相同才算同源。跨源的前端请求,浏览器会先判断是否放行,拦的是 JS 读响应,不是请求发不出去。

简单请求 vs 预检

  • 简单请求:GET / 部分 POST、无自定义头,直接发,靠响应头 Access-Control-Allow-Origin 决定能否读;
  • 预检:带自定义头或非简单方法时,浏览器先发 OPTIONS 探路,通过才发真实请求。

常见错误配置

  1. 盲目返回 *:又带凭据就矛盾,且等于对任何源开放;
  2. 漏了预检头:只配了主请求,OPTIONS 被拒,真实请求根本到不了;
  3. 用代理代替 CORS:能解但把跨域搬到了服务端,仍要控制信任。

实战案例:三个真实报错

  1. 响应头重复:Nginx 与后端框架各加了一次 Access-Control-Allow-Origin,浏览器只接受一个值,报 The 'Access-Control-Allow-Origin' header contains multiple values。用 curl -I 看响应头,只保留一处。
  2. OPTIONS 返回 405:预检请求被当成普通请求拒绝,真实请求根本发不出去。修复:让网关或框架对 OPTIONS 直接返回 204,并回填允许的方法与头。
  3. 带 Cookie 就失败:跨域要同时满足前端 credentials: 'include' 与后端 Access-Control-Allow-Credentials: true,且 Allow-Origin 不能是 *,必须回显具体来源。

常见问题(FAQ)

CORS 是后端漏洞吗?不是,它是浏览器强制的安全策略;后端返回的头只是“授权”,不放行则脚本读不到响应。为什么请求发出去了仍报错?因为拦的是“读响应”,请求本身已到达服务器——所以写操作仍要做鉴权与幂等。端口不同算同源吗?不算,协议、域名、端口任一不同即跨源。本地开发怎么处理?用开发服务器代理(如 Vite 的 server.proxy),比放宽 CORS 安全得多。返回 * 就够了吗?公开只读接口可以;涉及凭据或内部数据必须白名单校验来源。

CORS 与 CSRF 的关系

这两个概念经常被混为一谈,但方向完全不同:

  • CORS 保护的是“读取”:它限制的是 A 站点能否读到 B 站点的响应内容,属于浏览器保护用户数据不被跨站读取的机制;
  • CSRF 攻击的是“写入”:攻击者并不需要读取响应,只要让浏览器带上用户的 Cookie 发出请求即可,因此宽松的 CORS 并不是 CSRF 的成因;
  • 因此两者的防御也不通用:CORS 无法防住 CSRF,必须依靠 SameSite、CSRF Token 或校验 Origin。

与之相关的几个头

  • Access-Control-Allow-Origin 只控制读取,不控制是否发出请求;
  • Access-Control-Max-Age 决定预检结果的缓存时长,太小会导致每个请求都先发一次 OPTIONS,明显增加延迟;
  • Vary: Origin 在使用动态回显来源时必不可少,否则 CDN 会把一个来源的响应缓存后发给另一个来源。

把这些配合好,既能开放必要的跨域能力,又不会把权限边界弄丢。

调试跨域问题的实操步骤

  1. 先区分请求是否真正发出:打开网络面板观察是否存在预检请求。若预检失败,真实请求根本不会发送,此时修改业务逻辑毫无意义。
  2. 完整查看响应头:跨域错误的原因往往写在响应头里,直接查看实际返回的头,比根据报错文字猜测更快。
  3. 确认来源是否被回显:使用凭据时来源必须精确匹配而不能使用通配,回显错误或缺少相应声明都会导致失败。
  4. 用命令行复现:用命令行工具手动带上来源头发起请求并观察响应,能排除浏览器缓存与前端代码的干扰,快速确认服务端行为。
  5. 区分开发与生产配置:开发环境常用宽松配置以便联调,务必确认生产配置是独立且更严格的,避免把调试配置带上线。

跨域问题的本质是配置问题而非代码错误,按上述顺序验证可以把排查范围快速缩小到具体的一个响应头。