1. 简介

如今,我们访问的网页在展示数据时,常常需要向不同服务器发起请求。这种跨域通信机制被称为 跨域资源共享(CORS)。本文将深入讲解什么是 CORS、浏览器如何实现 CORS 策略,以及为什么需要 预检请求(Preflight Request)

2. 同源策略(Same Origin Policy)是什么?

在介绍 CORS 之前,先来了解浏览器的一项基础安全机制:同源策略(SOP)。SOP 的作用是限制一个源(origin)下的脚本与其他源的资源进行交互。

在 SOP 中,只有当两个 URL 的 协议(scheme)域名(domain)端口(port) 完全一致时,才被视为同源。例如:

  • https://www.baeldung.com ✅ 和 https://www.baeldung.com/about ✅ 是同源
  • https://www.baeldung.com ❌ 和 http://www.baeldung.com ❌ 不是同源(协议不同)

浏览器通过 SOP 限制脚本使用 XMLHttpRequestFetch API 等方式向其他源发起请求,从而防止跨站请求伪造等安全问题。

3. 什么是 CORS?

CORS 是一种机制,允许一个源的脚本向另一个源发起请求。它通过一组特定的 HTTP 头部,让服务器决定是否允许跨域请求。浏览器根据这些头部来决定是否将响应返回给前端脚本。

根据浏览器处理方式的不同,CORS 请求分为三类:

3.1. 简单请求(Simple Requests)

满足以下条件的请求被称为简单请求:

  • 请求方法是 GETHEADPOST
  • 请求头只能是自动添加的 User-Agent,或以下白名单中的头部:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限 application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 不使用 ReadableStream 对象
  • 没有为 XMLHttpRequest.upload 添加事件监听器

例如,以下代码发起一个简单请求:

const xhr = new XMLHttpRequest(); 
const url = 'https://www.api.com?q=test'; 
xhr.open('GET', url); 
xhr.onreadystatechange = requestHandler; 
xhr.send();

浏览器会自动在请求头中添加 Origin,服务器则通过 Access-Control-Allow-Origin 响应头告知是否允许该来源访问资源。

✅ 示例流程如下图所示:

Simple Request

如果服务器未设置该头部或来源不匹配,浏览器将阻止脚本访问响应内容,并在控制台报错:

❌ 示例错误如下图所示:

Screenshot-2021-01-13-at-22.56.52

3.2. 非简单请求(Non-Simple Requests)

不符合简单请求条件的请求被称为 非简单请求(Preflighted Request)。浏览器会在正式请求前发送一个 OPTIONS 类型的预检请求,询问服务器是否允许该请求。

预检请求包含以下关键头部:

  • Origin:请求来源
  • Access-Control-Request-Method:请求方法
  • Access-Control-Request-Headers:自定义请求头

服务器响应时需返回以下头部:

  • Access-Control-Allow-Origin:允许的来源
  • Access-Control-Allow-Methods:允许的方法
  • Access-Control-Allow-Headers:允许的请求头
  • Access-Control-Max-Age:预检结果缓存时间(秒)

举个例子,如果我们在请求中加入自定义头部:

const xhr = new XMLHttpRequest();
const url = 'https://www.api.com?q=test';
xhr.open('GET', url);
xhr.setRequestHeader('custom-header', 'test');
xhr.onreadystatechange = requestHandler;
xhr.send();

浏览器会先发送预检请求:

✅ 成功响应后,正式请求才会被发出:

Screenshot-2021-01-13-at-23.13.47

如果服务器未返回正确响应头,浏览器将阻止请求,并在控制台报错:

❌ 示例错误如下图所示:

Screenshot-2021-01-13-at-23.19.52

3.3. 带凭证的请求(Credentialed Requests)

CORS 默认不允许携带凭证(如 Cookie、Authorization 头、TLS 客户端证书)。若需要携带,必须在请求中设置 withCredentials = true,并且服务器必须返回以下两个头部:

  • Access-Control-Allow-Origin:不能是 *,必须指定具体来源
  • Access-Control-Allow-Credentials: true

示例代码如下:

const xhr = new XMLHttpRequest();
const url = 'https://www.api.com?q=test';
xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();

如果服务器未正确配置,浏览器将阻止请求并报错:

❌ 示例错误如下图所示:

Screenshot-2021-01-13-at-23.28.02

4. 为什么需要预检请求?

CORS 是在同源策略之后提出的,为了兼容旧系统并增强安全性,引入了预检请求机制。它的作用如下:

保护服务器安全:防止服务器因未知请求而产生副作用(如删除数据)
动态适应变化:服务器可以根据请求动态决定是否允许特定方法或头部
兼容性好:对于不支持 CORS 的老系统,浏览器默认按不允许跨域处理

5. 总结

CORS 是现代 Web 开发中解决跨域问题的核心机制。浏览器将跨域请求分为三类:

请求类型 是否需要预检 特点说明
简单请求 满足特定条件,直接发送请求
非简单请求 需要预检,服务器确认后才发送正式请求
带凭证的请求 可能需要 ✅ 必须服务器允许,且来源不能为 *

理解这些机制有助于我们更好地处理跨域场景,避免踩坑。


原始标题:Cross-Origin Resource Sharing and Why We Need Preflight Requests