什么是跨域
CORS跨域”通常是指跨域资源共享(Cross-Origin Resource Sharing,简称 CORS)中的“跨域”概念。这里的CORS其实是“Cross-Origin”的简写,意思就是“跨源”或“跨域”。
当一个网页(比如 https://a.com 里的页面)尝试去访问另一个不同源(https://b.com)的资源(比如请求 API、加载图片、发送 Ajax 等),就构成了跨域请求。
同源策略(Same-Origin Policy)是浏览器的一个安全机制:协议、域名、端口三者必须完全一致,才被认为是“同源”。只要有一个不同,就是跨域。例如:
http://a.com与https://a.com(协议不同)→ 跨域http://a.com与http://b.com(域名不同)→ 跨域http://a.com:80与http://a.com:8080(端口不同)→ 跨域
为什么会有跨域限制? 浏览器为了安全,防止恶意网站通过脚本窃取其他网站的数据。没有同源策略,你在访问 bank.com 时打开的另一个恶意网页就可以读取你的银行信息。
简单请求与非简单请求
浏览器在处理跨域请求时,根据请求的特征将其分为两类:
- 简单请求
- 非简单请求(也叫预检请求)
简单请求必须同时满足以下所有条件:
- 请求方法必须是以下之一:
GET、POST、HEAD - 只能使用以下简单请求头(不能有自定义头,如
X-Custom)。允许的请求头包括:AcceptAccept-LanguageContent-LanguageContent-Type(但值必须满足下面的限制)DPR、Downlink、Save-Data、Viewport-Width、Width(这些是较新的,一般忽略)
Content-Type的值只能是以下三种之一application/x-www-form-urlencodedmultipart/form-datatext/plain
- 请求中没有使用
ReadableStream对象(极少用到,忽略)
非简单请求(即需要预检的请求):
- 使用了
PUT、DELETE、PATCH、CONNECT、OPTIONS、TRACE等方法 - 或者使用了自定义请求头(如
X-Requested-With、Authorization、X-Custom等) - 或者
Content-Type不是上面三种,比如application/json、application/xml
非简单请求流程
sequenceDiagram
participant Browser as 浏览器 (https://a.com)
participant ServerB as 服务器 (https://b.com)
Note over Browser: 1. JS 代码发起 fetch('https://b.com/api', { method: 'PUT', headers: { 'X-Custom': 'value' } })
Browser->>ServerB: 2. OPTIONS /api (预检请求)<br/>Origin: https://a.com<br/>Access-Control-Request-Method: PUT<br/>Access-Control-Request-Headers: X-Custom
ServerB-->>Browser: 3. 预检响应<br/>Access-Control-Allow-Origin: https://a.com<br/>Access-Control-Allow-Methods: PUT<br/>Access-Control-Allow-Headers: X-Custom<br/>Access-Control-Max-Age: 86400
Note over Browser: 4. 检查预检响应头,通过则继续
Browser->>ServerB: 5. 真实请求 PUT /api<br/>Origin: https://a.com<br/>X-Custom: value
ServerB-->>Browser: 6. 真实响应<br/>Access-Control-Allow-Origin: https://a.com<br/>响应体数据
Note over Browser: 7. 检查真实响应头,通过后将数据交给 JS
前端代码发起请求
你的网页(https://a.com)里的 JavaScript 执行了类似下面的代码:
fetch('https://b.com/api', {
method: 'PUT',
headers: { 'X-Custom': 'value' }
})
因为请求方法是 PUT(不是 GET/POST 等简单方法),且带自定义头 X-Custom,浏览器判定这是一个非简单请求。
浏览器自动发送 OPTIONS 预检请求
在发真实请求之前,浏览器先向 https://b.com/api 发一个 OPTIONS 请求,这个请求里会带上三个关键头:
Origin: https://a.com— 告诉服务器是哪个源发起的。Access-Control-Request-Method: PUT— 真实请求将使用的方法。Access-Control-Request-Headers: X-Custom— 真实请求中携带的自定义头。
这个预检请求的目的是试探服务器是否允许这种跨域请求。
服务器返回预检响应
服务器 b.com 收到 OPTIONS 请求后,可以返回一组 CORS 响应头,例如:
Access-Control-Allow-Origin: https://a.com
Access-Control-Allow-Methods: PUT, GET, POST
Access-Control-Allow-Headers: X-Custom
Access-Control-Max-Age: 86400
Allow-Origin告诉浏览器允许https://a.com这个源访问。Allow-Methods明确允许 PUT 方法。Allow-Headers明确允许X-Custom这个头。Max-Age表示这次预检的结果可以缓存 86400 秒,期间同一请求不再发预检。
上面的这些都需要在服务器端配置,也就是服务器端需要根据前端发出的请求去配置上面这些内容,才能解决跨域问题。
服务器不会主动终止请求。对于预检请求(OPTIONS)或正式请求,服务器都会正常接收、处理并返回响应(比如返回 200 或 403)。
浏览器检查预检响应
浏览器收到预检响应后,会严格检查:
- 如果没有
Access-Control-Allow-Origin或它的值不是https://a.com(也不是*),预检失败,浏览器直接报错,后续真实请求不会发送。 - 如果
Allow-Methods里没有PUT,同样失败。 - 如果
Allow-Headers里没有X-Custom,失败。
只有当所有条件满足,浏览器才认为预检通过,继续第 5 步。
浏览器在检查响应头后,决定终止后续动作:
- 如果预检响应中缺少
Access-Control-Allow-Origin等必要字段,浏览器就不发送真实的 PUT/POST 请求。 - 如果真实请求的响应缺少 CORS 头,浏览器不把响应数据交给 JavaScript,并在控制台报错。
所以,跨域错误的本质是浏览器拒绝放行,而不是服务器拒绝服务。服务器其实已经把响应发回来了,只是浏览器把它“吞掉”了。
浏览器发送真实请求
预检通过后,浏览器发送真实的 PUT 请求,请求头中会自动带上 Origin: https://a.com,以及你设置的自定义头 X-Custom: value。
服务器返回真实响应
服务器正常处理请求并返回数据,同时在响应头里必须再次包含 Access-Control-Allow-Origin: https://a.com(或 *),否则浏览器仍然会拦截。
注意:对于真实响应,服务器不需要再返回 Allow-Methods 等头,但 Allow-Origin 是必须的。
浏览器检查真实响应
浏览器检查真实响应的 Access-Control-Allow-Origin:
- 如果匹配当前源,浏览器将响应体交给 JavaScript 的
fetch或xhr,你的代码就能拿到数据。 - 如果不匹配,浏览器拦截响应,控制台报错,你的代码拿不到任何数据(虽然服务器已经处理并返回了)。
X-Custom
X-Custom 是一个自定义 HTTP 请求头的例子。在 Web 开发中,开发者经常需要传递一些标准协议里没有的额外信息,比如客户端版本、请求追踪 ID、自定义认证 token 等。为了区别于标准头,早期习惯用 X- 前缀来命名自定义头(比如 X-Request-ID、X-User-Token)。现在虽然 RFC 6648 不再推荐使用 X- 前缀,但很多项目依然沿用这种方式。
X-Custom 只是一个占位名字,你可以用任何自定义头(如 X-Auth-Token、My-App-Id 等)。
只要请求中带了任何自定义头,就会触发 CORS 预检,并且服务器必须在 Access-Control-Allow-Headers 中显式列出该头,否则请求会被浏览器拦截。
例如,上面的除了authorization之外,都可以算是X-Custom,是用户(阿里云 OSS )自定义头。
简单请求流程
sequenceDiagram
participant Browser as 浏览器 (https://a.com)
participant ServerB as 服务器 (https://b.com)
Note over Browser: 1. JS 代码发起 GET 请求<br/>fetch('https://b.com/api', { method: 'GET' })
Browser->>ServerB: 2. 实际请求 GET /api<br/>Origin: https://a.com
ServerB-->>Browser: 3. 响应 + CORS 头<br/>Access-Control-Allow-Origin: https://a.com<br/>响应体数据
Note over Browser: 4. 检查响应头中是否有<br/> Access-Control-Allow-Origin 且匹配当前源
alt 检查通过
Note over Browser: 5. 将响应数据交给 JS
else 检查失败
Note over Browser: 6. 拦截响应,控制台报错<br/>JS 拿不到数据
end
前端代码发起请求
你的网页(https://a.com)里的 JavaScript 执行类似下面的代码:
fetch('https://b.com/api', {
method: 'GET', // 简单方法
headers: {
'Content-Type': 'text/plain' // 简单 Content-Type
}
})
因为请求方法是 GET、POST 或 HEAD,且自定义头只有简单头(如 Accept、Accept-Language、Content-Language、Content-Type 且值为 application/x-www-form-urlencoded、multipart/form-data 或 text/plain),浏览器将其判定为简单请求。
浏览器直接发送实际请求
浏览器不会先发 OPTIONS 预检,而是直接发出 GET 请求。请求头中自动添加 Origin: https://a.com,告诉服务器这个请求来自哪个源。
服务器返回响应
服务器收到请求后正常处理,并在响应头中必须包含 Access-Control-Allow-Origin 字段,例如:
Access-Control-Allow-Origin: https://a.com
或者 *(允许任意源)。如果服务器没有返回这个头,或者返回的值不包含 https://a.com(且不是 *),浏览器就会拦截。
浏览器检查响应头
浏览器检查响应头中的 Access-Control-Allow-Origin:
- 有且值匹配当前源(或
*)→ 检查通过 - 没有该头,或值不匹配 → 检查失败
将数据交给 JavaScript
检查通过后,浏览器允许你的 fetch 或 XMLHttpRequest 拿到响应数据(如 JSON、文本等),代码正常执行。
拦截响应,控制台报错
如果检查失败,浏览器不会把响应数据传递给 JavaScript,同时控制台报错(如 No 'Access-Control-Allow-Origin' header is present)。注意:服务器其实已经返回了数据,但被浏览器藏起来了。
解决跨域的常用手段
CORS(跨域资源共享)—— 最标准、最推荐
原理:服务器在响应头中明确告知浏览器允许哪些源访问。
实现:后端配置响应头
Access-Control-Allow-Origin: https://a.com 或 *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom, Content-Type
优点:功能完整,支持所有 HTTP 方法,安全性可控。
缺点:需要后端配合修改;非简单请求会有一次 OPTIONS 预检(但可缓存)。
适用:任何需要跨域调用 API 的场景,尤其是生产环境。
2. 代理转发(同源策略绕过)
原理:让前端请求同源的代理服务器,代理服务器再去请求真正的目标服务器,然后将结果返回给前端。浏览器看到的始终是同源请求。
实现方式:
- 开发环境:Webpack DevServer 的
proxy配置、Vite 的proxy、Create React App 的proxy。 - 生产环境:Nginx 反向代理、Node.js 中间层(如 Express + http-proxy-middleware)。
示例(Webpack):
devServer: {
proxy: {
'/api': 'https://b.com'
}
}
优点:前端代码无需改动,完全避开跨域问题;可以额外处理鉴权、缓存等。
缺点:需要额外部署代理服务;增加了请求链路。
适用:前后端分离项目开发阶段;没有 CORS 控制权限的老接口;需要聚合多个外部 API 的场景。