跨域资源共享 (CORS) 与同源策略
面试回答: 同源策略是浏览器限制跨源脚本读取敏感数据的安全边界,同源要求协议、主机和端口一致。CORS 是服务器通过 HTTP 响应头声明跨源读取权限、浏览器负责执行校验的协议。满足安全列表条件的请求可以直接发送,否则浏览器会先发
OPTIONS预检;无论是否预检,实际响应都必须通过 CORS 校验。CORS 只决定前端能否读取响应,不负责身份认证,也不能替代 CSRF 防护。
1. 同源策略 (Same-Origin Policy)
浏览器中的一个源(Origin)由 URL 的协议、主机和端口组成,三者完全一致才属于同源。
以 https://www.example.com:443 为基准:
同源策略不是“禁止一切跨源请求”,而是限制不同源之间的交互能力,尤其是读取数据:
- 跨源脚本不能随意读取另一个页面的 DOM;
localStorage、IndexedDB 等存储按源隔离;fetch/ XHR 可以发出部分跨源请求,但脚本读取响应需要通过 CORS;- 表单提交、链接跳转以及
<img>、<script>等资源嵌入在一定条件下可以跨源。
这个边界可以避免恶意页面直接读取用户在网银、邮箱等其他网站中的敏感页面和数据。
同源不等于同站。 Cookie 的 Domain、Path、Secure、SameSite 等规则与 Origin 判断不是同一套模型。例如两个子域可能属于同站,但仍然不同源。分析 Cookie、CORS 和 CSRF 时必须分别判断。
2. CORS (Cross-Origin Resource Sharing)
CORS 是建立在 HTTP 之上的一套协议:服务器通过响应头声明“这个响应可以共享给哪些源”,浏览器负责校验并决定是否把响应内容交给前端 JavaScript。
因此,CORS 主要限制的是跨源响应能否被前端读取,不等于请求一定没有发送到服务器。使用 curl、Postman 或服务端代码发请求时,也不会受到浏览器 CORS 校验的限制。
一个典型的跨源 fetch / XHR 请求流程是:
- 浏览器判断目标 URL 是否跨源。
- 判断请求是否满足 CORS 安全列表;满足则直接发送,否则先发预检请求。
- 浏览器在请求中添加
Origin,服务器返回相应的Access-Control-Allow-*响应头。 - 浏览器校验响应头;校验失败时,前端只能得到类似网络错误的结果,不能读取响应内容。
CORS 请求通常分为:简单请求(更准确地说是满足 CORS 安全列表的请求)和需要预检的请求。
(1) 简单请求
简单请求不会先发 OPTIONS,但必须同时满足以下核心条件:
- 请求方法只能是
GET、HEAD或POST。 - 只能使用 CORS 安全列表中的请求头,例如
Accept、Accept-Language、Content-Language、Content-Type和满足限制的Range。 - 如果设置了
Content-Type,其媒体类型只能是:application/x-www-form-urlencodedmultipart/form-datatext/plain
- 不能主动携带
Authorization、X-Token等非安全列表请求头。
规范对安全列表请求头的值、长度和字符还有更细的限制。面试中最常见的判断点是:请求方法、
Content-Type和自定义请求头。
例如,下面的跨源请求可以直接发送:
服务器允许该源读取响应时,可以返回:
如果服务器没有返回匹配的 Access-Control-Allow-Origin,这个 POST 请求通常已经到达服务器并可能被执行,只是浏览器不会把响应开放给前端读取。这也是为什么 CORS 不能替代 CSRF 防护、身份认证和权限校验。
Vary: Origin 适合服务器根据请求的 Origin 动态返回不同 CORS 响应头的场景,可以避免共享缓存错误地复用其他源的响应。
(2) 预检请求(Preflight)
只要不满足简单请求条件,浏览器就会在实际请求前自动发送 OPTIONS 预检。例如:
- 使用
PUT、PATCH、DELETE等非安全列表方法; - 发送
Content-Type: application/json; - 添加
Authorization、X-Token等非安全列表请求头。
因此,POST 并不一定是简单请求,常见的 POST + application/json 就会触发预检。
假设前端准备发送:
浏览器会先自动发送:
服务器可以返回一个成功响应,常见写法是:
浏览器确认目标源、方法和请求头都被允许后,才会继续发送实际的 PUT 请求。需要特别注意:
OPTIONS预检通过,不代表实际响应自动通过;实际响应仍然要返回匹配的Access-Control-Allow-Origin。- 如果实际请求携带凭证,预检响应和实际响应还需要正确配置
Access-Control-Allow-Credentials: true。 - 预检请求本身不会携带跨源 Cookie 等凭证,服务端不应只依赖登录 Cookie 来放行
OPTIONS。 Access-Control-Max-Age缓存的是预检结果,它使用浏览器内部独立的预检缓存;浏览器可以限制最大缓存时间或提前清除缓存。- 预检由浏览器自动完成,前端不能通过手动添加 CORS 响应头来跳过它。
(3) 常用 CORS 请求头与响应头
即使 CORS 请求成功,前端默认也只能读取 Cache-Control、Content-Language、Content-Length、Content-Type、Expires、Last-Modified 和 Pragma 等安全列表响应头。读取自定义响应头时,需要服务端显式暴露:
(4) 高频误区
mode: "no-cors"不能解决跨域。 它会限制可用的方法和请求头,并让前端得到不可读取内容和响应头的 opaque response。- CORS 不是服务端防盗链或接口鉴权。 非浏览器客户端可以直接请求接口,服务端仍需进行身份认证和权限校验。
- 不要无条件回显任意
Origin。 需要携带凭证时,应维护可信源白名单,并返回匹配的具体源。 - 请求成功不代表 CORS 成功。 Network 面板可能看到服务器返回
200,但只要 CORS 响应头不匹配,前端仍然无法读取响应。
规范依据:Fetch Standard:CORS protocol。
3. 携带 Cookie 的跨源请求
以 fetch 为例,跨源请求默认不会因为存在登录 Cookie 就自动携带凭证。需要同时满足以下条件:
- 前端设置
credentials: 'include';XHR 则设置withCredentials = true。 - Cookie 自身的 Domain、Path、Secure 和 SameSite 规则允许发送。
- 服务端返回匹配当前请求源的
Access-Control-Allow-Origin,不能使用*。 - 服务端返回
Access-Control-Allow-Credentials: true。
浏览器是否发送 Cookie 与前端是否能读取响应是两层判断。即使 Cookie 已发送,响应缺少正确的 CORS 头时,前端仍然读不到响应内容。
4. JSONP (旧方案)
JSONP 利用 <script> 标签不受同源策略限制。
- 原理:动态插入
<script src="https://api.example.com?callback=fn">,服务端返回可执行代码fn({ data: ... })。 - 限制:只支持 GET,缺少标准的错误处理,并且等同于执行对方返回的脚本,存在明显安全风险。
现代项目应优先使用 CORS,JSONP 主要用于理解历史跨域方案。
5. 其他解决方案
- 开发服务器或 Nginx 反向代理:浏览器请求同源地址,由服务器转发到真正的后端。服务端之间的请求不执行浏览器 CORS 校验。
postMessage:用于窗口、标签页和 iframe 之间的跨源消息通信。接收方必须校验event.origin,发送方不要随意使用*作为目标源。- WebSocket:握手不使用 CORS 协议,但浏览器会发送
Origin,服务端仍应校验允许的来源并完成鉴权。
这些方案解决的问题不同:代理用于 HTTP 接口转发,postMessage 用于窗口通信,WebSocket 用于双向长连接,不能互相当作通用替代品。
6. 面试总结
- 同源比较的是协议、主机和端口;同源与 Cookie 的同站规则不同。
- CORS 由服务端声明授权、浏览器执行校验,非浏览器客户端不受这套读取限制。
- 简单请求直接发送;不满足安全列表条件的请求先预检,再发送实际请求。
- CORS 失败不代表请求一定没到服务器,所以它不能替代鉴权、权限校验和 CSRF 防护。
- 凭证请求需要前后端和 Cookie 属性共同满足条件,且
Access-Control-Allow-Origin不能为*。

