跨域是前端面试里最容易“答得像背诵”的题:同源策略、CORS、JSONP、反向代理……概念大家都会说,但一旦追问“为什么浏览器要拦”“什么时候会触发预检”“为什么后端明明返回 200,前端还是报错”,很多回答就不够完整了。
这篇文章会把跨域问题从浏览器安全模型一直讲到 CORS 头部与预检流程。
一、先说本质:什么叫跨域?
“跨域”其实不是 HTTP 协议里的词,而是浏览器安全模型里的词。
浏览器判断两个地址是否同源,看的不是“域名像不像”,而是三件事:
- 协议
- 主机
- 端口
只有这三者都相同,才算同源。
例如:
| URL | 是否与 https://example.com:443 同源 |
|---|---|
https://example.com | 是 |
http://example.com | 否,协议不同 |
https://api.example.com | 否,主机不同 |
https://example.com:8443 | 否,端口不同 |
所以“跨域”的本质就是:
当前页面的源,和它想访问的目标资源的源,不相同。
二、为什么浏览器要限制跨域?
因为如果没有限制,恶意网站就可以直接读取你在其他站点的敏感数据。
比如:
- 你登录了银行网站
- 浏览器里还带着银行 Cookie
- 你又打开了一个恶意网站
- 恶意网站如果能随意发请求并读取银行响应
- 你的隐私、账单、账户信息就可能泄露
所以浏览器引入了同源策略(Same-Origin Policy)。
它的核心目标是:
限制一个源的脚本随意读取另一个源的敏感资源。
同源策略示意图如下:
![]()
注意,同源策略不是“阻止请求发出去”,而是重点限制:
- 读取跨源响应
- 访问跨源 DOM
- 获取跨源上下文中的敏感数据
三、跨域不是“所有跨源请求都不允许”
这是很重要的一个细节。
浏览器并不是完全禁止跨源资源加载。
例如这些通常是允许的:
<img src="..."><script src="..."><link href="..."><video src="...">
原因是浏览器允许“跨源加载”,但通常会限制“跨源读取”。
最典型的例子:
- 页面可以显示跨域图片
- 但如果你把这张跨域图片画到 canvas 上,再尝试读像素,浏览器就会因为安全原因阻止你
所以你可以把跨域问题拆成两句话:
- 请求可能发得出去
- 但响应不一定允许前端脚本读取
四、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, OPTIONS5.4 Access-Control-Allow-Headers
告诉浏览器允许哪些自定义请求头:
Access-Control-Allow-Headers: Content-Type, Authorization5.5 Access-Control-Allow-Credentials
表示是否允许浏览器在跨域请求中携带凭证:
Access-Control-Allow-Credentials: true5.6 Access-Control-Max-Age
表示预检结果可以缓存多久:
Access-Control-Max-Age: 86400六、简单请求:不是所有跨域请求都会先发 OPTIONS
很多人以为只要跨域就一定预检,这其实不对。
浏览器把一部分满足条件的请求视为简单请求,这类请求不会先发预检。
简单请求通常需要同时满足:
- 方法是
GET、HEAD、POST - 手动设置的请求头属于 safelist
Content-Type属于安全范围,例如:application/x-www-form-urlencodedmultipart/form-datatext/plain
例如:
fetch('https://api.example.com/data')如果服务端返回:
Access-Control-Allow-Origin: https://app.example.com浏览器就允许前端读取响应。
MDN 的简单请求流程图如下:
七、预检请求:为什么会先发一个 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.1Origin: https://app.example.comAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: content-type, authorization这个请求的意思就是:
“我接下来想从这个源发一个 POST,请求头里会带上这些字段,你允不允许?”
如果服务器同意,才会发真正的 POST。
预检流程图如下:
八、预检响应里服务端要返回什么?
服务端至少要返回能让浏览器通过校验的头部,例如:
HTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Methods: POST, GET, OPTIONSAccess-Control-Allow-Headers: Content-Type, AuthorizationAccess-Control-Max-Age: 86400浏览器会逐项检查:
- 当前源是否被允许
- 方法是否被允许
- 请求头是否被允许
只要有一项不符合,前端脚本就无法拿到响应。
九、为什么后端明明返回 200,前端还是报跨域?
这是一个非常高频的排查问题。
原因在于:
CORS 错误是浏览器拦的,不是服务器拦的。
也就是说,后端完全可能已经返回了一个 200 响应,但浏览器发现:
Access-Control-Allow-Origin不匹配- 缺少
Access-Control-Allow-Credentials - 预检没通过
Access-Control-Allow-Headers不完整
于是直接把响应拦掉,不允许 JavaScript 读取。
所以前端看到的往往是:
- 控制台报 CORS 错误
- Network 里其实请求已经发出并收到响应
这个现象一定要能解释清楚。
十、为什么 * 和 Cookie 不能一起用?
如果跨域请求要带 Cookie:
前端需要:
fetch(url, { credentials: 'include'})服务端需要:
Access-Control-Allow-Credentials: trueAccess-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 是浏览器与服务器协作下的授权机制。