这是一个非常好的问题!简单来说:跨域限制是浏览器主动实施的,服务器可以参与控制这个过程,但最终是浏览器决定是否拒绝请求。
让我详细解释一下:
1. 浏览器是执行者
- 跨域请求(CORS - Cross-Origin Resource Sharing)限制是浏览器的安全策略
- 当你从一个源(如
https://example.com)的网页中向另一个源(如https://api.other.com)发送AJAX/fetch请求时,浏览器会自动:- 检查请求是否是"简单请求"还是"非简单请求"
- 对于非简单请求,会先发送一个预检请求(OPTIONS请求)
- 根据服务器返回的CORS头部决定是否允许实际请求
2. 服务器的角色
服务器通过响应头告诉浏览器:
Access-Control-Allow-Origin:允许哪些源访问Access-Control-Allow-Methods:允许哪些HTTP方法Access-Control-Allow-Headers:允许哪些请求头Access-Control-Allow-Credentials:是否允许发送凭据(如cookies)
3. 拒绝请求的场景
情况A:浏览器直接拒绝(没有CORS配置时)
javascript
// 假设当前页面是 https://example.com
fetch('https://api.other.com/data') // 不同域
.then(...)
// 浏览器会直接报错:Cross-Origin Request Blocked
// 请求甚至没有到达 api.other.com
情况B:服务器拒绝,浏览器执行拒绝
javascript
// 服务器返回的响应头:
// Access-Control-Allow-Origin: https://allowed.com
// 而当前源是 https://example.com
// 浏览器检查后发现不被允许,拒绝访问响应数据
4. 例外情况
- 非浏览器环境:Node.js、Postman、curl等工具没有跨域限制
- 服务端到服务端:服务器之间的请求不受CORS限制
- 某些HTML标签 :
<img>、<script>、<link>等可以跨域加载资源
关键要点
- 浏览器主动实施:跨域是浏览器的安全策略
- 服务器被动响应:服务器通过响应头告诉浏览器自己的跨域策略
- 协作机制:浏览器检查服务器的CORS头部后决定是否允许请求继续
这就是为什么在开发时,前端遇到跨域问题需要后端配置CORS头部,或者在开发环境中使用代理服务器来解决跨域问题。
5. 时序图说明
这是一个跨域请求的时序图,展示浏览器、前端代码和服务器之间的交互流程:
sequenceDiagram
participant F as 前端代码
(https://example.com) participant B as 浏览器 participant S as 服务器
(https://api.other.com) Note over F,S: 1. 简单请求(无预检) F->>B: 发送fetch()请求
GET /api/data B->>S: 直接发送请求 S-->>B: 返回响应 + CORS头
(Access-Control-Allow-Origin) B->>B: 检查CORS头
当前源是否在允许列表中? alt CORS允许 B-->>F: 返回响应数据 else CORS拒绝 B-->>F: 抛出跨域错误
响应被浏览器屏蔽 end Note over F,S: 2. 非简单请求(有预检) F->>B: 发送fetch()请求
PUT /api/data + 自定义头 B->>S: 发送OPTIONS预检请求 S-->>B: 返回预检响应
包含CORS策略头 B->>B: 检查预检响应 alt 预检通过 B->>S: 发送实际PUT请求 S-->>B: 返回实际响应 + CORS头 B-->>F: 返回数据 else 预检拒绝 B-->>F: 直接报错
实际请求不会发送 end
(https://example.com) participant B as 浏览器 participant S as 服务器
(https://api.other.com) Note over F,S: 1. 简单请求(无预检) F->>B: 发送fetch()请求
GET /api/data B->>S: 直接发送请求 S-->>B: 返回响应 + CORS头
(Access-Control-Allow-Origin) B->>B: 检查CORS头
当前源是否在允许列表中? alt CORS允许 B-->>F: 返回响应数据 else CORS拒绝 B-->>F: 抛出跨域错误
响应被浏览器屏蔽 end Note over F,S: 2. 非简单请求(有预检) F->>B: 发送fetch()请求
PUT /api/data + 自定义头 B->>S: 发送OPTIONS预检请求 S-->>B: 返回预检响应
包含CORS策略头 B->>B: 检查预检响应 alt 预检通过 B->>S: 发送实际PUT请求 S-->>B: 返回实际响应 + CORS头 B-->>F: 返回数据 else 预检拒绝 B-->>F: 直接报错
实际请求不会发送 end
时序流程说明:
第一阶段:前端代码发起请求
- 前端JavaScript调用
fetch()或XMLHttpRequest - 浏览器接收到请求指令
第二阶段:浏览器判断请求类型
bash
简单请求条件(同时满足):
- 方法:GET、POST、HEAD
- 头部:仅限安全头部(Accept、Accept-Language等)
- Content-Type:text/plain、application/x-www-form-urlencoded、multipart/form-data
第三阶段:分叉处理
A. 简单请求路径:
- 浏览器直接发送请求到服务器
- 服务器返回响应和CORS头部
- 浏览器检查CORS头部,决定是否将响应给前端代码
B. 非简单请求路径(需要预检):
diff
触发预检的情况:
- 方法:PUT、DELETE、PATCH等
- 自定义头部:X-Custom-Header等
- Content-Type:application/json等
- 浏览器先发送
OPTIONS预检请求 - 服务器返回CORS策略
- 关键决策点 :浏览器检查策略
- 通过 → 发送实际请求
- 拒绝 → 直接报错,不发送实际请求
第四阶段:最终结果
- 成功 :数据传递给前端代码的
then()或onload - 失败 :触发
catch()或onerror,控制台显示跨域错误
关键决策点时序:
graph TD
A[前端发起请求] --> B{浏览器判断
简单请求?} B -->|是| C[直接发送请求] B -->|否| D[发送OPTIONS预检] D --> E{服务器返回
CORS策略} E -->|允许| F[发送实际请求] E -->|拒绝| G[直接报错] C --> H[服务器返回响应] F --> H H --> I{浏览器检查
CORS头} I -->|源允许| J[传递数据给前端] I -->|源拒绝| K[屏蔽响应并报错]
简单请求?} B -->|是| C[直接发送请求] B -->|否| D[发送OPTIONS预检] D --> E{服务器返回
CORS策略} E -->|允许| F[发送实际请求] E -->|拒绝| G[直接报错] C --> H[服务器返回响应] F --> H H --> I{浏览器检查
CORS头} I -->|源允许| J[传递数据给前端] I -->|源拒绝| K[屏蔽响应并报错]
实际报错示例时间点:
- 预检阶段失败:服务器未返回正确的OPTIONS响应
- 响应阶段失败 :
Access-Control-Allow-Origin不匹配当前源 - 凭证请求失败 :设置了
credentials: 'include'但服务器未返回Access-Control-Allow-Credentials: true
这个时序图清晰地展示了跨域限制是浏览器主动实施的过程,服务器只是被动地响应浏览器的询问(通过CORS头部)。