Appearance
同源策略与 CORS 跨域原理
难度等级:🟡 中级(2-4年)
分类:浏览器安全
归档日期:2026-09-03
📚 理论知识
一、同源策略是什么,为什么存在
同源的定义:协议(protocol)、域名(host)、端口(port)三者完全相同才算同源,少一个都不算。比如 https://a.com 和 http://a.com 协议不同不同源,https://a.com 和 https://b.a.com 域名不同不同源,https://a.com 和 https://a.com:8080 端口不同也不同源。
为什么存在:同源策略是浏览器最核心的安全基石,目的是隔离不同来源的页面,防止恶意网站读取或操纵其他网站的数据。设想没有这个限制:你打开一个恶意网站,它用 JS 发一个请求到你的银行网站,因为浏览器会带上你银行网站的登录 Cookie,请求本身能成功,如果没有同源策略限制读取响应,这个恶意网站就能直接读到你银行账户的余额、交易记录——同源策略挡住的正是这最后一步"读取响应内容"的权限。
二、同源策略限制了什么,没限制什么
限制的:
- Ajax(
XMLHttpRequest/fetch)跨域请求的响应读取(请求本身其实已经发出去了,只是浏览器不让 JS 拿到返回结果)。 - 跨域的 DOM 互相访问(比如 iframe 内外两个不同源页面,父页面不能直接操作子 iframe 里的 DOM)。
- 跨域读取 Cookie、localStorage、IndexedDB 这类存储(存储是按源隔离的,
a.com的页面读不到b.com存的数据)。
不限制的(容易被忽略、但是很多攻击的根源):
<script src>、<img src>、<link href>、<iframe src>这类标签发起的跨域资源加载天生就是允许的——这也是为什么 CDN 能跨域加载 JS/CSS/图片,早期 JSONP 也是利用<script>标签不受同源策略限制这个特性实现的。- 表单跨域提交同样不受限制——这正是 CSRF 攻击能够成立的前提:攻击者不需要"读"响应,只需要让请求"发出去"就够了,而这一步同源策略从来没管过。
三、CORS(跨域资源共享)原理
CORS 是在同源策略基础上,由服务端通过响应头显式声明"我允许哪些来源读取这个响应",浏览器负责在前端强制执行这个约定。
关键理解:跨域请求实际上已经发到服务端了,服务端该处理的逻辑也执行了(比如一次跨域的删除操作,数据库记录确实被删了),CORS 控制的只是"浏览器要不要把这个响应结果交给发起请求的 JS 代码"——如果服务端没有返回允许的 CORS 响应头,浏览器就会拦截响应,JS 那边拿到的是一个网络错误,但服务端的操作可能已经生效了。这个细节经常被面试官拿来追问"跨域请求到底发没发出去"。
四、简单请求 vs 预检请求(Preflight)
浏览器把跨域请求分成两类:
简单请求(直接发送,不需要预检)需要同时满足:
- 方法是
GET、HEAD、POST之一; Content-Type只能是application/x-www-form-urlencoded、multipart/form-data、text/plain之一;- 没有自定义请求头(比如没有手动加
Authorization、X-Custom-Header这类)。
预检请求(Preflight):只要不满足上面任一条件(比如用了 PUT/DELETE、Content-Type 是 application/json、带了自定义头),浏览器会先自动发一个 OPTIONS 请求去问服务端"我接下来要发这样一个跨域请求,你允许吗",服务端在 OPTIONS 的响应里通过 Access-Control-Allow-Methods、Access-Control-Allow-Headers 等头告知允许的范围,浏览器确认允许后才会真正发出原始请求。这也是为什么前端项目里接口大量使用 application/json 时,会看到 Network 面板里每个跨域请求都多出一个 OPTIONS 请求。
五、Access-Control-Allow-Origin: * 和携带凭证为什么冲突
浏览器规范明确规定:如果请求需要携带凭证(Cookie,即 fetch 里设置了 credentials: 'include' 或 XHR 设置了 withCredentials = true),服务端的 Access-Control-Allow-Origin 不能设置成通配符 *,必须回显一个具体的、明确的 Origin 值,否则浏览器会直接拦截响应并报错。
原因:如果允许 * 配合凭证一起放行,就意味着任意网站都能带着用户在目标网站的登录 Cookie 发请求并读到响应,同源策略和 CORS 存在的意义就荡然无存了。这个限制是浏览器强制的安全底线,不是配置失误就能绕过的。
常见误区
| 问题 | 要点 |
|---|---|
| 认为跨域请求"发不出去" | 错。简单请求实际上已经发到服务端并执行了,只是浏览器拦截了响应不让 JS 读到;只有预检失败的情况下,真正的请求才不会被发出。 |
| 认为同源策略只是浏览器"不让发跨域请求" | 错。同源策略从不限制请求的发出(表单、<img>、<script> 都能跨域发),它限制的是响应能不能被读到,以及 DOM/存储的跨域访问。 |
| 认为配置 CORS 就是"前端解决跨域"的方案 | 不准确。CORS 的许可必须由服务端配置响应头才能生效,前端单方面加不了任何请求头就能绕过 CORS——前端能做的是配开发环境代理(如 webpack devServer proxy)规避跨域,或者用 JSONP 这种历史方案,本质都不是"前端解决",而是"绕开或不触发"跨域限制。 |
| 认为 CORS 是为了"防跨域攻击" | 不完全准确。CORS 的本质是一种放行机制,是服务端主动放宽同源策略限制、允许指定来源访问自己资源的方式,而不是一种额外的攻击防御手段。 |
❌ 错误答法示范
错误示例 1:「跨域请求会被浏览器直接拦截,请求根本不会发到服务端。」
❌ 问题:不准确。简单请求会正常发出并执行,只是响应结果被浏览器拦下不给 JS;只有预检请求(OPTIONS)没有得到服务端允许时,真正的业务请求才不会被发送。混淆"请求没发出"和"响应没读到"是这道题最常见的硬伤。
错误示例 2:「后端把 Access-Control-Allow-Origin 设成 * 就能解决所有跨域问题,包括带 Cookie 的请求。」
❌ 问题:* 只能用于不携带凭证的跨域请求。一旦前端设置了 withCredentials: true 或 credentials: 'include',服务端必须回显具体的 Origin(而不是 *),并且额外设置 Access-Control-Allow-Credentials: true,否则浏览器会拦截响应。
🎤 模拟面试作答
这道题我先说同源策略本身,再展开到 CORS 是怎么在它基础上工作的。
同源策略是协议、域名、端口三者都相同才算同源,是浏览器最基础的安全机制,目的是隔离不同网站之间的数据访问,防止恶意网站读取别的网站的隐私数据。它限制的其实是"读"——限制 Ajax 跨域请求读响应、限制跨域 DOM 互访、限制跨域读 Cookie/Storage,但它不限制"发",比如 <script>、<img> 标签跨域加载资源是允许的,表单跨域提交也是允许的,这也是为什么 CSRF 能钻这个空子,因为它根本不需要读响应,只需要让请求发出去、让服务端执行操作。
CORS 就是在同源策略基础上,由服务端通过响应头主动声明"我允许哪些源读取我的响应",浏览器负责强制执行这个约定。这里有个容易被问到的细节:跨域请求其实已经真正发到服务端了,该执行的逻辑服务端也执行了,CORS 控制的只是"浏览器要不要把这个响应结果交给发起方的 JS",如果没有对应的许可头,JS 拿到的是网络错误,但数据库操作可能已经生效了。
浏览器会把跨域请求分成简单请求和预检请求两类。简单请求满足几个条件——方法是 GET/HEAD/POST,Content-Type 是表单类型或纯文本,没有自定义头——会直接发送;不满足的话,比如用了 PUT、或者 Content-Type 是 application/json,浏览器会先发一个 OPTIONS 预检请求去问服务端允不允许,服务端通过 Access-Control-Allow-Methods 这些头确认了,浏览器才会真正发出原始请求。
还有一个经常被追问的点是,如果请求要带 Cookie,也就是设置了 withCredentials,服务端的 Access-Control-Allow-Origin 就不能设成通配符 *,必须回显具体的 Origin,因为如果允许通配符加凭证一起放行,就等于任何网站都能带着用户的登录状态读到目标网站的数据,这是浏览器强制的安全底线。
如果面试官感兴趣,我还可以聊聊开发环境里 webpack devServer 的代理是怎么绕开跨域限制的,本质上跟真正的 CORS 授权是两回事。
❓ 面试官追问预测
追问 1:既然跨域的简单请求已经发到服务端并执行了,那如果这是一个有副作用的操作(比如删除数据),是不是意味着即使前端拿不到响应,删除操作也已经生效了?这样安全吗?
应对思路:是的,会生效。这正是为什么服务端不能仅仅依赖"CORS 会挡住跨域请求"来做安全防护——CORS 保护的是"数据不被跨域读走",不是"操作不被跨域触发"。真正防止未授权跨域操作,需要依赖身份鉴权(比如校验 Token/Session 是否合法)和 CSRF 防御(SameSite Cookie、CSRF Token),CORS 和 CSRF 防御是两套互补的机制,不能互相替代。
追问 2:为什么 <script> 标签能跨域加载 JS,而 Ajax 不行?这个不一致的设计是历史遗留问题吗?
应对思路:某种程度上是历史遗留——
<script>、<img>这类标签的跨域加载能力在同源策略概念成型之前就已经存在(Web 早期就需要跨站加载资源、图片这些基础能力),而 Ajax(XMLHttpRequest)是后来才出现的、专门用来读取结构化数据的能力,被纳入了更严格的同源限制里。JSONP 正是利用了这个"历史缝隙"——用<script src>加载一段跨域返回的、会立即执行并回调的 JS 代码来模拟数据获取,是 CORS 出现之前的权宜方案。
追问 3:预检请求(OPTIONS)会不会影响接口性能?有什么优化手段?
应对思路:会,每次跨域的非简单请求理论上都要多一次 OPTIONS 往返。优化手段是服务端在预检响应里设置
Access-Control-Max-Age,告诉浏览器这次预检结果可以缓存多久(比如设成 86400 秒),在这个时间窗口内浏览器对同样的跨域请求(同源、同方法、同请求头组合)不会重复发预检,直接复用之前的许可结果。
追问 4:前端项目开发时用 webpack devServer 配置 proxy 解决跨域,这个原理和 CORS 是一回事吗?
应对思路:不是一回事。devServer proxy 本质是让浏览器只跟本地的 devServer(同源)通信,真正的跨域请求是由 Node 服务器(devServer)在服务端发起转发给真实的后端接口,服务端与服务端之间不存在同源策略限制。所以这是"绕开"浏览器的跨域限制,而不是像 CORS 那样"经过服务端授权"合法跨域,生产环境不能靠这个方案,通常改用 Nginx 反向代理达到类似效果,或者后端直接配置 CORS。