前言
在学习如何解决跨域问题的时候,了解到了在使用 CORS(跨域资源共享)时,会存在预检请求(Preflight Request)的情况
产生原因
CORS 将请求分为简单请求和非简单请求。对于简单请求,浏览器会直接发送请求,并在请求头中添加 Origin 字段,服务器根据该字段判断是否允许跨域请求。而对于非简单请求,浏览器在正式发送请求之前,会先发送一个预检请求(OPTIONS 请求),以确认服务器是否允许该跨域请求。
简单请求和非简单请求的判断标准
简单请求
需同时满足以下条件:
-
请求方法:只能是
GET、POST、HEAD之一。 -
请求头:除了浏览器自动设置的请求头(如
Connection、User - Agent等),只允许包含以下几个自定义请求头:AcceptAccept-LanguageContent-LanguageContent-Type,且其值只能是application/x-www-form-urlencoded、multipart/form-data或text/plain。
非简单请求
不满足简单请求条件的请求即为非简单请求,例如:
- 请求方法为
PUT、DELETE等。 Content-Type的值为application/json等。
预检请求的工作流程
-
发送预检请求:当浏览器检测到非简单请求时,会先发送一个
OPTIONS请求到服务器,该请求包含以下重要的请求头:Origin:表示请求的源地址。Access-Control-Request-Method:表示实际请求将使用的 HTTP 方法。Access-Control-Request-Headers:表示实际请求将携带的自定义请求头。
-
服务器响应预检请求:服务器接收到预检请求后,会根据请求头中的信息判断是否允许该跨域请求。如果允许,服务器会在响应头中添加以下信息:
Access-Control-Allow-Origin:指定允许访问该资源的源地址,可以是具体的域名,也可以是*表示允许所有域名。Access-Control-Allow-Methods:指定允许的 HTTP 方法。Access-Control-Allow-Headers:指定允许的自定义请求头。Access-Control-Max-Age:指定预检请求的缓存时间,单位为秒。在缓存时间内,浏览器不会再次发送预检请求。
-
发送实际请求:如果预检请求得到了服务器的允许,浏览器会发送实际的请求,并在请求头中添加
Origin字段。服务器在处理实际请求时,同样需要在响应头中添加Access-Control-Allow-Origin等相关信息。
示例代码
服务器端代码(Node.js + Express)
const express = require('express');
const app = express();
// 处理预检请求
app.options('/api', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400'); // 预检请求缓存 24 小时
res.status(204).send();
});
// 处理实际请求
app.put('/api', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
const data = { message: 'Hello, CORS with preflight!' };
res.json(data);
});
const port = 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
客户端代码(使用 fetch API)
const options = {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify({ data: 'example' })
};
fetch('http://example.com/api', options)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
注意事项 之 Access-Control-Max-Age
预检请求会增加额外的网络开销,因为浏览器需要先发送一个
OPTIONS请求。可以通过设置Access-Control-Max-Age来缓存预检请求,减少不必要的请求。
预检请求的额外开销
当浏览器发起非简单的跨域请求时,会先发送一个预检请求(OPTIONS 请求)。这个额外的请求会带来多方面的开销:
时间开销
- 每次发送非简单跨域请求前都要先发送预检请求,等待服务器响应。例如在高并发场景下,页面上多个组件同时发起非简单跨域请求,就会导致大量的预检请求排队等待处理,增加了整体的响应时间,影响用户体验。
带宽开销
- 预检请求本身也需要占用网络带宽。虽然
OPTIONS请求通常请求体较小,但在频繁发起非简单跨域请求的情况下,这些额外的请求累计起来也会消耗不少的带宽资源。
服务器负载
- 服务器需要处理这些额外的
OPTIONS请求,对服务器资源造成一定的压力。尤其是对于高流量的网站,大量的预检请求会增加服务器的 CPU 和内存消耗。
Access-Control-Max-Age 缓存机制
为了减少预检请求带来的额外开销,可以通过设置 Access-Control-Max-Age 响应头来缓存预检请求的结果。
工作原理
- 当服务器在响应预检请求时设置了
Access-Control-Max-Age响应头,浏览器会将该预检请求的结果进行缓存。在指定的缓存时间(以秒为单位)内,如果再次发起相同的非简单跨域请求,浏览器将直接发送实际请求,而不再发送预检请求。
小Tips
- 服务器在处理预检请求时,应该返回
204 No Content状态码,表示请求成功但没有响应体。