2611 words
13 minutes
跨域问题的本质与 CORS 的完整流程

跨域是前端面试里最容易“答得像背诵”的题:同源策略、CORS、JSONP、反向代理……概念大家都会说,但一旦追问“为什么浏览器要拦”“什么时候会触发预检”“为什么后端明明返回 200,前端还是报错”,很多回答就不够完整了。

这篇文章会把跨域问题从浏览器安全模型一直讲到 CORS 头部与预检流程


一、先说本质:什么叫跨域?#

“跨域”其实不是 HTTP 协议里的词,而是浏览器安全模型里的词。

浏览器判断两个地址是否同源,看的不是“域名像不像”,而是三件事:

  1. 协议
  2. 主机
  3. 端口

只有这三者都相同,才算同源。

例如:

URL是否与 https://example.com:443 同源
https://example.com
http://example.com否,协议不同
https://api.example.com否,主机不同
https://example.com:8443否,端口不同

所以“跨域”的本质就是:

当前页面的源,和它想访问的目标资源的源,不相同。


二、为什么浏览器要限制跨域?#

因为如果没有限制,恶意网站就可以直接读取你在其他站点的敏感数据。

比如:

  1. 你登录了银行网站
  2. 浏览器里还带着银行 Cookie
  3. 你又打开了一个恶意网站
  4. 恶意网站如果能随意发请求并读取银行响应
  5. 你的隐私、账单、账户信息就可能泄露

所以浏览器引入了同源策略(Same-Origin Policy)

它的核心目标是:

限制一个源的脚本随意读取另一个源的敏感资源。

同源策略示意图如下:

Same-origin policy

注意,同源策略不是“阻止请求发出去”,而是重点限制:

  • 读取跨源响应
  • 访问跨源 DOM
  • 获取跨源上下文中的敏感数据

三、跨域不是“所有跨源请求都不允许”#

这是很重要的一个细节。

浏览器并不是完全禁止跨源资源加载。

例如这些通常是允许的:

  • <img src="...">
  • <script src="...">
  • <link href="...">
  • <video src="...">

原因是浏览器允许“跨源加载”,但通常会限制“跨源读取”。

最典型的例子:

  • 页面可以显示跨域图片
  • 但如果你把这张跨域图片画到 canvas 上,再尝试读像素,浏览器就会因为安全原因阻止你

所以你可以把跨域问题拆成两句话:

  1. 请求可能发得出去
  2. 但响应不一定允许前端脚本读取

四、CORS 是什么?#

CORS 全称:

Cross-Origin Resource Sharing

中文通常翻译为:

跨源资源共享

它不是用来“取消同源策略”的,而是给服务器一个能力:

服务器可以明确告诉浏览器:哪些源可以访问我。

也就是说,CORS 的本质是:

浏览器继续执行同源策略,但允许服务端通过 HTTP 头对特定跨域请求进行授权。


五、CORS 的最核心头部有哪些?#

5.1 Origin#

浏览器发起跨域请求时,会自动带上:

Origin: https://foo.example

它表示:这个请求来自哪个源。

5.2 Access-Control-Allow-Origin#

服务器响应时会告诉浏览器:

Access-Control-Allow-Origin: https://foo.example

或:

Access-Control-Allow-Origin: *

意思是:这个源是否被允许读取响应。

5.3 Access-Control-Allow-Methods#

用于预检响应里,告诉浏览器允许哪些方法:

Access-Control-Allow-Methods: GET, POST, OPTIONS

5.4 Access-Control-Allow-Headers#

告诉浏览器允许哪些自定义请求头:

Access-Control-Allow-Headers: Content-Type, Authorization

5.5 Access-Control-Allow-Credentials#

表示是否允许浏览器在跨域请求中携带凭证:

Access-Control-Allow-Credentials: true

5.6 Access-Control-Max-Age#

表示预检结果可以缓存多久:

Access-Control-Max-Age: 86400

六、简单请求:不是所有跨域请求都会先发 OPTIONS#

很多人以为只要跨域就一定预检,这其实不对。

浏览器把一部分满足条件的请求视为简单请求,这类请求不会先发预检。

简单请求通常需要同时满足:

  1. 方法是 GETHEADPOST
  2. 手动设置的请求头属于 safelist
  3. Content-Type 属于安全范围,例如:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

例如:

fetch('https://api.example.com/data')

如果服务端返回:

Access-Control-Allow-Origin: https://app.example.com

浏览器就允许前端读取响应。

MDN 的简单请求流程图如下:

Simple CORS request


七、预检请求:为什么会先发一个 OPTIONS?#

当请求“看起来不再像普通表单提交”时,浏览器会更谨慎。

例如下面这种请求:

fetch('https://api.example.com/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer xxx'
},
body: JSON.stringify({ name: 'Owen' })
})

它通常会触发预检。

原因是:

  • Content-Type: application/json 不在简单请求白名单里
  • Authorization 是自定义/非 safelist 请求头

于是浏览器不会立刻发真正请求,而是先发一个:

OPTIONS /user HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

这个请求的意思就是:

“我接下来想从这个源发一个 POST,请求头里会带上这些字段,你允不允许?”

如果服务器同意,才会发真正的 POST。

预检流程图如下:

Preflighted CORS request


八、预检响应里服务端要返回什么?#

服务端至少要返回能让浏览器通过校验的头部,例如:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

浏览器会逐项检查:

  1. 当前源是否被允许
  2. 方法是否被允许
  3. 请求头是否被允许

只要有一项不符合,前端脚本就无法拿到响应。


九、为什么后端明明返回 200,前端还是报跨域?#

这是一个非常高频的排查问题。

原因在于:

CORS 错误是浏览器拦的,不是服务器拦的。

也就是说,后端完全可能已经返回了一个 200 响应,但浏览器发现:

  • Access-Control-Allow-Origin 不匹配
  • 缺少 Access-Control-Allow-Credentials
  • 预检没通过
  • Access-Control-Allow-Headers 不完整

于是直接把响应拦掉,不允许 JavaScript 读取。

所以前端看到的往往是:

  • 控制台报 CORS 错误
  • Network 里其实请求已经发出并收到响应

这个现象一定要能解释清楚。


如果跨域请求要带 Cookie:

前端需要:

fetch(url, {
credentials: 'include'
})

服务端需要:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://app.example.com

注意:这时 Access-Control-Allow-Origin 不能是 *

原因很简单:

如果允许任意源都带凭证读取响应,那就等于把用户身份数据向全网开放了。

所以浏览器会强制要求:

  • 带凭证请求时,必须是明确源
  • 不能用通配符 *

十一、CORS 解决的是“浏览器读取权限”,不是“服务器访问控制”#

这是一个非常容易被混淆的点。

CORS 不是鉴权系统。

它不能替代:

  • 登录态校验
  • Token 校验
  • 权限控制
  • 防刷策略

因为 CORS 只约束浏览器。

换句话说:

  • Postman 不受 CORS 限制
  • curl 不受 CORS 限制
  • 服务端之间调用不受 CORS 限制

所以如果一个接口“只靠 CORS 保安全”,那是不够的。


十二、常见跨域解决方案有哪些?#

12.1 CORS(现代主流方案)#

优点:

  • 标准方案
  • 浏览器原生支持
  • 适合前后端分离

缺点:

  • 需要后端正确配置
  • 涉及预检与凭证细节

12.2 反向代理#

例如本地开发常见:

  • Vite proxy
  • Webpack devServer proxy
  • Nginx 反向代理

原理其实不是“浏览器跨域成功了”,而是:

浏览器始终请求同源代理服务器,由代理服务器再去请求真实后端。

对浏览器来说它没跨域。

12.3 JSONP#

历史方案,利用 <script> 标签可以跨域加载脚本的特性。

缺点很明显:

  • 只支持 GET
  • 安全性与可维护性差
  • 现代项目基本不推荐

12.4 postMessage#

用于不同窗口、iframe、webview 之间安全通信,不是传统 AJAX 跨域方案,但经常和跨域一起考。


十三、跨域和 CSRF 有什么关系?#

很多人会把跨域和 CSRF 混在一起。

它们不是同一个问题。

跨域#

核心是:

浏览器是否允许前端脚本读取跨源响应。

CSRF#

核心是:

攻击者诱导用户浏览器带着已有登录态,向目标站点发起恶意请求。

注意:

  • 同源策略并不能阻止表单提交式的 CSRF
  • CORS 也不是专门防 CSRF 的方案

所以服务端仍然需要:

  • CSRF Token
  • SameSite Cookie
  • Referer / Origin 校验

十四、真实项目中最常见的 CORS 配置错误#

14.1 漏掉 OPTIONS#

预检请求走的是 OPTIONS,如果服务器没处理,会直接失败。

14.2 Access-Control-Allow-Headers 不完整#

比如前端发了 Authorization,后端没允许这个头。

14.3 误以为 * 能和凭证一起用#

这是典型配置错误。

14.4 只改前端,不改后端#

CORS 本质是服务端授权,前端单改 mode: 'cors' 没用。

14.5 只看状态码,不看浏览器控制台#

CORS 问题往往必须结合:

  • Console
  • Network
  • Response Headers
  • Preflight Request

一起排查。


十五、面试里怎么回答“跨域问题与 CORS”?#

建议按这个结构回答:

第一步:先讲跨域的本质#

跨域是浏览器基于同源策略产生的安全限制,本质是当前页面源和目标资源源不同。

第二步:讲同源策略限制的是什么#

它主要限制脚本读取跨源响应、访问跨源 DOM 等敏感操作,不是简单地“禁止所有跨域请求”。

第三步:讲 CORS#

CORS 是服务端通过一组 HTTP 响应头,显式告诉浏览器哪些跨源请求可以被允许读取响应。

第四步:讲简单请求和预检请求#

  • 简单请求不会预检
  • 非简单请求会先发 OPTIONS 预检
  • 预检通过后才发真正请求

第五步:补充凭证和常见坑#

  • 带 Cookie 时不能用 *
  • 预检要处理 OPTIONS
  • CORS 不是鉴权机制

这样回答,基本就是一套完整答案了。


十六、总结#

跨域问题的本质,并不是“前端请求不了另一个域名”,而是:

浏览器为了保护用户安全,限制一个源的脚本随意读取另一个源的敏感响应。

而 CORS 的作用,就是让服务端有机会明确授权:

  • 哪些源能访问
  • 哪些方法能用
  • 哪些请求头能带
  • 是否允许带凭证

所以真正准确的总结应该是:

跨域是浏览器安全模型的问题,CORS 是浏览器与服务器协作下的授权机制。

跨域问题的本质与 CORS 的完整流程
https://fuwari.vercel.app/posts/cors-and-cross-origin/
Author
Owen
Published at
2026-05-29
License
CC BY-NC-SA 4.0