跨域是服务器拒绝请求还是浏览器去拒绝的请求?

133 阅读3分钟

这是一个非常好的问题!简单来说:跨域限制是浏览器主动实施的,服务器可以参与控制这个过程,但最终是浏览器决定是否拒绝请求。

让我详细解释一下:

1. 浏览器是执行者

  • 跨域请求(CORS - Cross-Origin Resource Sharing)限制是浏览器的安全策略
  • 当你从一个源(如 https://example.com)的网页中向另一个源(如 https://api.other.com)发送AJAX/fetch请求时,浏览器会自动:
    1. 检查请求是否是“简单请求”还是“非简单请求”
    2. 对于非简单请求,会先发送一个预检请求(OPTIONS请求)
    3. 根据服务器返回的CORS头部决定是否允许实际请求

2. 服务器的角色

服务器通过响应头告诉浏览器:

  • Access-Control-Allow-Origin:允许哪些源访问
  • Access-Control-Allow-Methods:允许哪些HTTP方法
  • Access-Control-Allow-Headers:允许哪些请求头
  • Access-Control-Allow-Credentials:是否允许发送凭据(如cookies)

3. 拒绝请求的场景

情况A:浏览器直接拒绝(没有CORS配置时)

// 假设当前页面是 https://example.com
fetch('https://api.other.com/data')  // 不同域
  .then(...)
// 浏览器会直接报错:Cross-Origin Request Blocked
// 请求甚至没有到达 api.other.com

情况B:服务器拒绝,浏览器执行拒绝

// 服务器返回的响应头:
// 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 前端代码<br>(https://example.com)
    participant B as 浏览器
    participant S as 服务器<br>(https://api.other.com)

    Note over F,S: 1. 简单请求(无预检)
    F->>B: 发送fetch()请求<br>GET /api/data
    B->>S: 直接发送请求
    S-->>B: 返回响应 + CORS头<br>(Access-Control-Allow-Origin)
    B->>B: 检查CORS头<br>当前源是否在允许列表中?
    alt CORS允许
        B-->>F: 返回响应数据
    else CORS拒绝
        B-->>F: 抛出跨域错误<br>响应被浏览器屏蔽
    end

    Note over F,S: 2. 非简单请求(有预检)
    F->>B: 发送fetch()请求<br>PUT /api/data + 自定义头
    B->>S: 发送OPTIONS预检请求
    S-->>B: 返回预检响应<br>包含CORS策略头
    B->>B: 检查预检响应
    alt 预检通过
        B->>S: 发送实际PUT请求
        S-->>B: 返回实际响应 + CORS头
        B-->>F: 返回数据
    else 预检拒绝
        B-->>F: 直接报错<br>实际请求不会发送
    end

时序流程说明:

第一阶段:前端代码发起请求

  1. 前端JavaScript调用fetch()XMLHttpRequest
  2. 浏览器接收到请求指令

第二阶段:浏览器判断请求类型

简单请求条件(同时满足):
- 方法:GET、POST、HEAD
- 头部:仅限安全头部(Accept、Accept-Language等)
- Content-Type:text/plain、application/x-www-form-urlencoded、multipart/form-data

第三阶段:分叉处理

A. 简单请求路径:

  • 浏览器直接发送请求到服务器
  • 服务器返回响应和CORS头部
  • 浏览器检查CORS头部,决定是否将响应给前端代码

B. 非简单请求路径(需要预检):

触发预检的情况:
- 方法:PUT、DELETE、PATCH等
- 自定义头部:X-Custom-Header等
- Content-Type:application/json等
  1. 浏览器先发送OPTIONS预检请求
  2. 服务器返回CORS策略
  3. 关键决策点:浏览器检查策略
    • 通过 → 发送实际请求
    • 拒绝 → 直接报错,不发送实际请求

第四阶段:最终结果

  • 成功:数据传递给前端代码的then()onload
  • 失败:触发catch()onerror,控制台显示跨域错误

关键决策点时序:

graph TD
    A[前端发起请求] --> B{浏览器判断<br>简单请求?}
    B -->|是| C[直接发送请求]
    B -->|否| D[发送OPTIONS预检]
    D --> E{服务器返回<br>CORS策略}
    E -->|允许| F[发送实际请求]
    E -->|拒绝| G[直接报错]
    C --> H[服务器返回响应]
    F --> H
    H --> I{浏览器检查<br>CORS头}
    I -->|源允许| J[传递数据给前端]
    I -->|源拒绝| K[屏蔽响应并报错]

实际报错示例时间点:

  • 预检阶段失败:服务器未返回正确的OPTIONS响应
  • 响应阶段失败Access-Control-Allow-Origin不匹配当前源
  • 凭证请求失败:设置了credentials: 'include'但服务器未返回Access-Control-Allow-Credentials: true

这个时序图清晰地展示了跨域限制是浏览器主动实施的过程,服务器只是被动地响应浏览器的询问(通过CORS头部)。