一、 一切的根源:同源策略 (Same-Origin Policy)
在谈论 CORS 之前,我们必须先理解它的“母亲”——同源策略 (SOP)。
这是一个由浏览器强制执行的、至关重要的安全模型。它规定:一个源(origin)的文档或脚本,不能与另一个源的资源进行交互。
什么是“源”?一个源由 协议(protocol)、域名(domain)、端口(port) 三者共同定义。只要有一个不同,就是不同的源。
| URL | 源 (Origin) | 与 http://www.example.com 是否同源? |
|---|---|---|
http://www.example.com/index.html | http://www.example.com | 是 |
https://www.example.com | https://www.example.com | 否 (协议不同) |
http://api.example.com | http://api.example.com | 否 (域名不同) |
http://www.example.com:8080 | http://www.example.com:8080 | 否 (端口不同) |
SOP 的目的: 想象一下,如果你在浏览器的一个标签页中登录了你的网上银行,然后在另一个标签页中打开了一个恶意网站。如果没有同源策略,那个恶意网站的脚本就可以轻易地向你的银行网站发送请求,读取你的账户信息、甚至进行转账操作,后果不堪设想。SOP 就是为了防止这种“跨站请求伪造”(CSRF)等攻击而存在的。
但是,随着 Web 应用越来越复杂,前后端分离成为主流,前端应用(如 http://www.myapp.com)需要请求后端 API(如 http://api.myapp.com)是家常便饭。这明显违反了同源策略。怎么办?
CORS (Cross-Origin Resource Sharing) 闪亮登场!
二、 CORS 是什么?
CORS 是一种机制,它使用额外的 HTTP 头来告诉浏览器,允许运行在一个源上的 Web 应用访问位于另一源上的特定资源。
核心思想: 跨源请求是否安全,不应该由浏览器“一刀切”地决定,而应该由**被请求的资源所在的服务器**来决定。
所以,请记住一个关键点:CORS 的整个流程是由浏览器自动触发和强制执行的,但配置和决定权在后端服务器。 前端开发者通常只需要正常发送请求即可(除了个别情况,后面会讲)。
CORS 将跨源请求分为两类:简单请求 和 非简单请求(通常需要预检)。
三、 简单请求 (Simple Request)
浏览器认为某些请求“风险较小”,符合特定条件的,就划为“简单请求”。它不会触发“预检”(Preflight)请求,而是直接发送。
满足以下所有条件,才算简单请求:
-
请求方法 (Method) 是以下三者之一:
GETHEADPOST
-
HTTP 头信息 (Headers) 不超出以下几种字段:
AcceptAccept-LanguageContent-LanguageContent-Type(但值仅限于application/x-www-form-urlencoded、multipart/form-data、text/plain这三种)DPRDownlinkSave-DataViewport-WidthWidth
简单请求的具体过程:
假设我们的前端页面源是 http://www.a.com,要请求 http://api.b.com/users。
-
浏览器端(自动行为):
- 浏览器发现这是一个跨源请求,并且判断它是一个“简单请求”。
- 在发送请求时,浏览器会自动在 HTTP 请求头中添加一个
Origin字段,表明请求来自哪个源。
GET /users HTTP/1.1 Host: api.b.com Origin: http://www.a.com Accept: */* -
服务器端(需要配置):
- 服务器收到请求,看到
Origin字段,知道了这是一个来自http://www.a.com的跨源请求。 - 服务器根据自己的 CORS 配置策略,判断是否允许这个源的请求。
- 如果允许,服务器在返回的响应头中,必须包含一个
Access-Control-Allow-Origin字段,其值可以是请求的源http://www.a.com,或者是*(表示接受任意源的请求)。
HTTP/1.1 200 OK Content-Type: application/json Access-Control-Allow-Origin: http://www.a.com {"data": [...]}- 如果不允许,服务器可以不返回这个头,或者返回一个错误的源。
- 服务器收到请求,看到
-
浏览器端(再次自动行为):
- 浏览器收到响应后,检查响应头中是否存在
Access-Control-Allow-Origin字段。 - 如果存在,且其值包含了当前的源 (
http://www.a.com) 或为*,浏览器就认为请求成功,将响应数据交给 JavaScript 回调函数处理。 - 如果不存在,或者值不匹配,浏览器就会拦截这个响应,认为请求失败。你在控制台会看到经典的 CORS 错误,即使服务器已经成功处理了请求并返回了 200 状态码,前端的 JavaScript 代码也拿不到任何响应数据。
- 浏览器收到响应后,检查响应头中是否存在
四、 非简单请求 / 复杂请求 (Preflighted Request)
只要不满足上述“简单请求”条件的任何一条,都会被视为“非简单请求”。例如:
- 使用了
PUT,DELETE,PATCH等方法。 Content-Type的值是application/json。- 请求头中包含了自定义的头部字段,如
X-Token。
对于这些“可能对服务器数据产生副作用”的请求,浏览器出于安全考虑,在发送真实请求之前,会先发送一个“预检请求”(Preflight Request)去“探探路”,询问服务器是否允许即将到来的真实请求。
预检请求的具体过程(两步走):
第 1 步:预检请求 (Preflight Request)
这是一个 OPTIONS 方法的请求,由浏览器自动发起。
-
浏览器端(自动行为):
- 浏览器准备发送一个
PUT请求到http://api.b.com/users/1,并带有Content-Type: application/json和自定义头X-Token。 - 浏览器判断这是一个非简单请求,于是先发送一个
OPTIONS预检请求。 - 这个预检请求包含了几个关键的头部字段:
Origin: 表明请求来源。Access-Control-Request-Method: 告知服务器,接下来的真实请求会使用什么 HTTP 方法(例如PUT)。Access-Control-Request-Headers: 告知服务器,接下来的真实请求会带有哪些自定义的头部字段(例如Content-Type和X-Token)。
OPTIONS /users/1 HTTP/1.1 Host: api.b.com Origin: http://www.a.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Content-Type, X-Token - 浏览器准备发送一个
-
服务器端(需要配置):
- 服务器收到这个
OPTIONS请求,解析出预检信息。 - 服务器检查并判断是否允许来自
http://www.a.com的、使用PUT方法的、并且带有Content-Type和X-Token头部的请求。 - 如果允许,服务器返回一个
200 OK或204 No Content的响应,并且响应头中必须包含以下几个关键字段来“授权”:Access-Control-Allow-Origin: 必须的,允许的源。Access-Control-Allow-Methods: 允许的请求方法列表,如GET, POST, PUT。Access-Control-Allow-Headers: 允许的请求头列表,如Content-Type, X-Token。Access-Control-Max-Age: (可选) 预检请求的有效时间(秒)。在此期间,浏览器无需为同样的请求再次发送预检请求,可以直接缓存结果。
HTTP/1.1 204 No Content Access-Control-Allow-Origin: http://www.a.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, X-Token Access-Control-Max-Age: 86400 - 服务器收到这个
-
浏览器端(再次自动行为):
- 浏览器收到预检响应,检查这些
Access-Control-*头是否满足即将发送的真实请求。 - 如果预检通过,浏览器才会继续发送第 2 步的真实请求。
- 如果预检失败(比如服务器没有返回相应的
Allow头),浏览器会直接报错,真实请求根本不会被发送出去。
- 浏览器收到预检响应,检查这些
第 2 步:真实请求 (Actual Request)
预检成功后,浏览器发送的真实请求过程就和简单请求一模一样了。
- 浏览器端: 发送真实的
PUT请求,同样会自动带上Origin头。PUT /users/1 HTTP/1.1 Host: api.b.com Origin: http://www.a.com Content-Type: application/json X-Token: some-auth-token {"name": "New Name"} - 服务器端: 收到真实请求,处理业务逻辑,并在响应头中再次带上
Access-Control-Allow-Origin。注意:这一步不能少! - 浏览器端: 收到响应,检查
Access-Control-Allow-Origin,如果匹配,则请求成功,数据交给 JS 处理。
五、 附带身份凭证 (Credentials) 的请求
默认情况下,跨源请求不会发送 Cookies、HTTP认证信息或客户端SSL证书等身份凭证。
如果需要发送,必须满足前后端双方的共同约定:
-
前端设置:
- 在使用
fetch时,设置credentials: 'include'。 - 在使用
XMLHttpRequest时,设置xhr.withCredentials = true。
- 在使用
-
后端设置:
- 服务器必须在响应头中返回
Access-Control-Allow-Credentials: true。 - 极其重要的一点: 当服务器返回
Access-Control-Allow-Credentials: true时,Access-Control-Allow-Origin的值不能是通配符*,必须是明确的、与请求头Origin值一致的源。这是出于安全考虑。
- 服务器必须在响应头中返回
总结
| 特性 | 简单请求 | 非简单请求 (预检) |
|---|---|---|
| 触发条件 | 方法为 GET/HEAD/POST,且头部简单 | 不满足简单请求条件的任何请求,如 PUT/DELETE、application/json、自定义头等 |
| 请求次数 | 1 次 | 2 次 (1次 OPTIONS 预检 + 1次真实请求) |
| 浏览器行为 | 直接发送请求,请求头自动加 Origin | 先发 OPTIONS 预检请求,带 Request-Method/Headers,通过后再发真实请求 |
| 服务器配置 | 响应头需包含 Access-Control-Allow-Origin | 预检响应需包含 Allow-Origin/Methods/Headers,真实响应也需包含 Allow-Origin |
| 核心目的 | 验证来源是否被允许 | 在执行可能修改数据的操作前,先确认服务器是否允许该操作 |
希望这份超详细的讲解能让你对 CORS 有一个全面而深刻的理解!