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