几分钟认识一下预检请求

262 阅读4分钟

前言

在学习如何解决跨域问题的时候,了解到了在使用 CORS(跨域资源共享)时,会存在预检请求(Preflight Request)的情况

产生原因

CORS 将请求分为简单请求和非简单请求。对于简单请求,浏览器会直接发送请求,并在请求头中添加 Origin 字段,服务器根据该字段判断是否允许跨域请求。而对于非简单请求,浏览器在正式发送请求之前,会先发送一个预检请求(OPTIONS 请求),以确认服务器是否允许该跨域请求。

简单请求和非简单请求的判断标准

简单请求

需同时满足以下条件:

  1. 请求方法:只能是 GETPOSTHEAD 之一。

  2. 请求头:除了浏览器自动设置的请求头(如 ConnectionUser - Agent 等),只允许包含以下几个自定义请求头:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type,且其值只能是 application/x-www-form-urlencodedmultipart/form-data 或 text/plain
非简单请求

不满足简单请求条件的请求即为非简单请求,例如:

  • 请求方法为 PUTDELETE 等。
  • Content-Type 的值为 application/json 等。

预检请求的工作流程

  1. 发送预检请求:当浏览器检测到非简单请求时,会先发送一个 OPTIONS 请求到服务器,该请求包含以下重要的请求头:

    • Origin:表示请求的源地址。
    • Access-Control-Request-Method:表示实际请求将使用的 HTTP 方法。
    • Access-Control-Request-Headers:表示实际请求将携带的自定义请求头。
  2. 服务器响应预检请求:服务器接收到预检请求后,会根据请求头中的信息判断是否允许该跨域请求。如果允许,服务器会在响应头中添加以下信息:

    • Access-Control-Allow-Origin:指定允许访问该资源的源地址,可以是具体的域名,也可以是 * 表示允许所有域名。
    • Access-Control-Allow-Methods:指定允许的 HTTP 方法。
    • Access-Control-Allow-Headers:指定允许的自定义请求头。
    • Access-Control-Max-Age:指定预检请求的缓存时间,单位为秒。在缓存时间内,浏览器不会再次发送预检请求。
  3. 发送实际请求:如果预检请求得到了服务器的允许,浏览器会发送实际的请求,并在请求头中添加 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 状态码,表示请求成功但没有响应体。