CORS 机制中的预检请求(Preflight Request):为什么 OPTIONS 请求总是先于复杂请求发送?

各位同仁,各位对网络安全和前端开发有深入兴趣的朋友们,大家好。

今天,我们将深入探讨一个在现代Web开发中至关重要,但又常常令人感到困惑的机制——跨域资源共享(CORS)中的预检请求(Preflight Request)。具体来说,我们将聚焦于一个核心问题:为什么在CORS机制下,OPTIONS 请求总是先于那些所谓的“复杂请求”发送?我们将从其诞生的背景、工作原理、安全考量,以及实际开发中的应用和最佳实践等多个维度进行剖析。

1. 跨域的起源与同源策略

在深入预检请求之前,我们必须先理解它所要解决的问题的根源:同源策略 (Same-Origin Policy, SOP)。同源策略是浏览器最核心的安全机制之一,它限制了从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这里的“源”由三个部分组成:协议(protocol)、域名(host)和端口(port)。只有当这三者都完全一致时,两个URL才被认为是同源的。

同源策略的核心目的在于:

  • 防止恶意网站读取用户敏感数据: 想象一下,你登录了银行网站,同时又打开了一个恶意网站。如果没有同源策略,恶意网站上的JavaScript就可以向银行网站发送请求,并读取你的账户信息,这无疑是灾难性的。
  • 防止恶意网站执行未授权操作: 恶意网站可以向其他网站发送请求,进行如转账、修改密码等操作。

同源策略的限制主要体现在以下几个方面:

  1. Cookie、LocalStorage 和 IndexDB 无法读取: 不同源的网站无法读取彼此的这些存储数据。
  2. DOM 无法获得: 不同源的网站无法获得彼此的DOM元素。
  3. AJAX 请求被限制: 最直接的影响就是JavaScript发起的XMLHttpRequest或Fetch API请求,如果目标资源与当前页面不同源,浏览器会阻止其获取响应。

然而,随着Web应用的日益复杂,微服务架构的兴起,以及前后端分离的普及,不同源之间进行数据交互的需求变得越来越普遍。例如,前端应用部署在 app.example.com,而其API服务部署在 api.example.com,或者一个CDN上的资源需要被多个域名下的网站使用。在这种情况下,严格的同源策略就成了阻碍。

为了在保证安全性的前提下,允许受控的跨域通信,W3C 标准组织推出了 跨域资源共享 (Cross-Origin Resource Sharing, CORS) 机制。CORS 允许浏览器向跨域服务器发出 XMLHttpRequestFetch 请求,从而克服了同源策略的限制。它的核心思想是:浏览器在发送跨域请求时,会在请求头中携带 Origin 字段,告知服务器请求的来源。服务器接收到请求后,根据自身配置判断是否允许该来源访问,并在响应头中添加 Access-Control-Allow-Origin 等字段,告知浏览器是否允许跨域。如果服务器允许,浏览器就会将响应内容暴露给前端JavaScript代码;否则,浏览器会阻止JavaScript获取响应,尽管请求可能已经发送并到达服务器。

2. CORS 请求的分类:简单请求与复杂请求

CORS 机制将跨域请求分为两大类:简单请求 (Simple Request)复杂请求 (Preflighted Request)。这两种请求在处理流程上有着显著的区别,而预检请求正是为了处理复杂请求而引入的。

2.1 简单请求 (Simple Request)

简单请求是指那些对服务器副作用较小、安全性风险相对较低的请求。浏览器会直接发送这类请求,而无需事先发送 OPTIONS 预检请求。

一个请求需要满足以下所有条件,才会被认为是简单请求:

  1. HTTP 方法:
    • GET
    • HEAD
    • POST
  2. HTTP 头信息:
    • 只能使用浏览器自动设置的头部(如 User-AgentAccept 等)。
    • 手动设置的头部只能是以下之一:
      • Accept
      • Accept-Language
      • Content-Language
      • Content-Type
      • Last-Event-ID
      • DPR
      • Save-Data
      • Viewport-Width
      • Width
    • Content-Type 的值只能是以下三种:
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

示例:简单请求的流程

  1. 浏览器发送请求: 浏览器直接发送实际的跨域请求,并在请求头中添加 Origin 字段。
    GET /api/data HTTP/1.1
    Host: api.example.com
    Origin: http://app.example.com
    User-Agent: Mozilla/5.0 ...
    Accept: application/json
  2. 服务器处理请求并响应: 服务器接收请求,检查 Origin 字段。如果允许 http://app.example.com 访问,则在响应头中添加 Access-Control-Allow-Origin 字段。

    HTTP/1.1 200 OK
    Content-Type: application/json
    Access-Control-Allow-Origin: http://app.example.com
    Content-Length: 123
    
    {"message": "Hello from API"}
  3. 浏览器接收响应: 浏览器检查响应头中的 Access-Control-Allow-Origin。如果其值匹配当前页面的 Origin 或为 *,则将响应内容暴露给前端JavaScript。否则,抛出CORS错误。

2.2 复杂请求 (Preflighted Request)

不符合简单请求条件的任何跨域请求都被称为复杂请求。这些请求在发送实际请求之前,会先发送一个 OPTIONS 类型的预检请求,以询问服务器是否允许该跨域操作。

复杂请求的常见场景包括:

  • HTTP 方法不是 GETHEADPOST 例如 PUTDELETEPATCH
  • 手动设置了非简单请求允许的 HTTP 头: 例如 X-Requested-WithAuthorizationCustom-Header 等自定义头。
  • Content-Type 的值不是 application/x-www-form-urlencodedmultipart/form-datatext/plain 例如 application/json

示例:复杂请求的触发

// 假设当前页面是 http://app.example.com
fetch('http://api.example.com/data', {
    method: 'PUT', // 非GET/HEAD/POST
    headers: {
        'Content-Type': 'application/json', // 非允许的三种Content-Type
        'X-Auth-Token': 'some-token' // 自定义头
    },
    body: JSON.stringify({ key: 'value' })
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

上述 fetch 请求满足了多个复杂请求的条件:PUT 方法、Content-Type: application/json 和自定义头 X-Auth-Token。因此,浏览器在发送实际的 PUT 请求之前,会先发送一个 OPTIONS 预检请求。

3. 预检请求 (Preflight Request) 的核心机制

现在,我们来到了本文的核心:预检请求,以及它为什么总是先于复杂请求发送。

3.1 什么是预检请求?

预检请求是一个使用 OPTIONS HTTP 方法的请求。它由浏览器自动发起,目的是询问服务器当前网页所在的域名是否在允许访问的列表中,以及实际请求所使用的 HTTP 方法和请求头是否得到服务器的允许。

简而言之,预检请求就是浏览器在执行一个“有潜在风险”的跨域操作之前,先向服务器进行一次“安全咨询”。

3.2 预检请求的流程

让我们详细分解一个复杂请求(例如前面提到的 PUT 请求)的完整流程:

阶段一:浏览器发送预检请求 (OPTIONS)

当浏览器识别出一个复杂请求时,它会首先构造并发送一个 OPTIONS 请求到目标服务器的相同URL路径。这个 OPTIONS 请求会携带一些特殊的请求头,用于向服务器声明后续实际请求的意图。

预检请求的示例 (由浏览器自动发送):

OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: http://app.example.com  # 告知服务器请求来源
Access-Control-Request-Method: PUT # 告知服务器实际请求将使用PUT方法
Access-Control-Request-Headers: Content-Type, X-Auth-Token # 告知服务器实际请求将携带这些自定义头
User-Agent: Mozilla/5.0 ...
Accept: */*
  • Origin: 必需。表明发起跨域请求的源。
  • Access-Control-Request-Method: 必需。告知服务器实际请求将使用的 HTTP 方法。
  • Access-Control-Request-Headers: 可选。告知服务器实际请求将携带的非简单请求头部列表。

阶段二:服务器处理预检请求并响应

服务器收到 OPTIONS 请求后,需要根据自身配置的CORS策略来判断是否允许后续的实际请求。它会检查 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers 等请求头。如果服务器允许,它会在响应头中包含一系列 Access-Control-Allow-* 字段,表明其CORS策略。

预检响应的示例 (由服务器返回):

HTTP/1.1 204 No Content # 204表示请求成功但没有响应体,或者200 OK
Access-Control-Allow-Origin: http://app.example.com # 允许的来源
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS # 允许的方法
Access-Control-Allow-Headers: Content-Type, X-Auth-Token # 允许的头
Access-Control-Max-Age: 86400 # 预检结果的缓存时间(秒)
Content-Length: 0
  • Access-Control-Allow-Origin: 必需。指定允许访问的源。可以是具体的域名,也可以是 * (允许所有源,但在携带凭证时不能为 *)。
  • Access-Control-Allow-Methods: 必需。指定服务器允许的 HTTP 方法列表。
  • Access-Control-Allow-Headers: 可选。指定服务器允许的自定义请求头列表。
  • Access-Control-Max-Age: 可选。指示预检请求的结果可以被缓存多长时间(秒)。在这个时间内,浏览器将不再为相同的复杂请求发送预检请求。

阶段三:浏览器判断预检结果

浏览器接收到预检响应后,会根据响应头中的 Access-Control-Allow-* 字段来判断是否允许实际请求的发送:

  • 如果 Access-Control-Allow-Origin 包含当前请求的 Origin
  • 如果 Access-Control-Allow-Methods 包含实际请求将使用的方法。
  • 如果 Access-Control-Allow-Headers 包含实际请求将使用的所有非简单请求头。

只要其中任何一个条件不满足,浏览器就会认为预检失败,并阻止实际请求的发送,同时在控制台抛出CORS错误。

阶段四:浏览器发送实际请求 (如果预检成功)

只有当预检请求成功,并且服务器明确表示允许该操作时,浏览器才会发送实际的跨域请求。

实际请求的示例 (由浏览器发送):

PUT /data HTTP/1.1
Host: api.example.com
Origin: http://app.example.com
Content-Type: application/json
X-Auth-Token: some-token
User-Agent: Mozilla/5.0 ...
Content-Length: 19

{"key": "value"}

阶段五:服务器处理实际请求并响应

服务器接收并处理实际请求,然后返回正常响应。

实际响应的示例 (由服务器返回):

HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: http://app.example.com # 实际请求的响应也需要包含此头
Content-Length: 20

{"status": "updated"}

阶段六:浏览器接收实际响应

浏览器再次检查实际响应头中的 Access-Control-Allow-Origin。如果它匹配 Origin,则将响应内容暴露给前端JavaScript。

4. 为什么 OPTIONS 请求总是先于复杂请求发送?——安全性的核心考量

现在,我们终于可以直面核心问题了:为什么浏览器要如此“麻烦”地多发送一个 OPTIONS 请求?为什么不能像简单请求一样直接发送实际请求,然后由服务器来决定是否允许呢?答案在于 安全性和对服务器的保护

这个机制的设计是为了保护那些没有被设计成支持CORS的“遗留”服务器 (legacy servers),或者说,是为了防止在未经服务器明确授权的情况下,对服务器进行可能具有破坏性或非预期的操作。

让我们更深入地分析其背后的逻辑:

4.1 保护遗留服务器免受未知副作用

在CORS标准制定之前,许多Web服务器已经存在并运行多年。这些服务器通常只期望接收 GETPOST 等常见请求,并且可能对某些自定义HTTP头或 PUT/DELETE 方法没有预期的处理逻辑。

  • HTTP 方法的语义: GETHEAD 方法被设计为幂等的,通常不应该有副作用(即不应该修改服务器上的数据)。POST 方法通常用于创建资源,但其副作用通常是可控且预期的。然而,PUT(更新资源)和 DELETE(删除资源)方法则明确设计为具有修改服务器状态的副作用。
  • 自定义 HTTP 头: 自定义 HTTP 头可能会触发服务器上特定的业务逻辑。例如,一个遗留服务器可能有一个自定义头 X-Delete-All-Data,如果携带这个头,服务器就会执行一个高风险操作。

如果没有预检请求,会发生什么?

假设一个恶意网站 malicious.com 想要攻击 bank.com 的用户。如果用户在浏览器中登录了 bank.com,同时又访问了 malicious.com

如果 malicious.com 可以直接发送一个复杂请求(例如 PUTDELETE 请求,或者带有自定义头的 POST 请求)到 bank.com 的API接口:

// 在 malicious.com 上运行的JS代码
fetch('https://bank.com/api/account/123', {
    method: 'DELETE', // 尝试删除用户账户
    headers: {
        'X-Confirm-Delete': 'yes', // 假设这是银行API的一个自定义确认头
        'Authorization': 'Bearer ' + user_session_token // 浏览器会自动携带用户的Cookie/Auth信息
    }
});
  1. 请求会发送到服务器: 即使 bank.com 没有配置CORS,或者不允许 malicious.com 访问,这个请求仍然会从浏览器发出,到达 bank.com 的服务器。
  2. 服务器可能会执行操作: bank.com 的服务器会接收到这个 DELETE 请求,并且可能会按照其既定逻辑执行删除操作,因为它无法区分这个请求是来自 bank.com 自己的前端,还是来自 malicious.com。服务器并不知道 Origin 头是什么,或者它可能根本不关心。
  3. 浏览器阻止响应,但操作已完成: 即使浏览器在收到响应后,因为同源策略(或CORS未通过)而阻止 malicious.com 的JavaScript读取响应内容,但对 bank.com 而言,删除操作已经成功执行了。这意味着用户的数据已经被删除,而恶意网站甚至不需要知道操作是否成功,因为它已经造成了损害。

预检请求的作用:

预检请求通过在实际操作发生之前,询问服务器是否“愿意”接受这种类型的跨域请求,从而优雅地解决了这个问题。

malicious.com 尝试发送上述 DELETE 请求时:

  1. 浏览器先发送 OPTIONS 预检请求:
    OPTIONS /api/account/123 HTTP/1.1
    Origin: https://malicious.com
    Access-Control-Request-Method: DELETE
    Access-Control-Request-Headers: X-Confirm-Delete, Authorization
  2. bank.com 的服务器处理 OPTIONS 请求:
    • 如果 bank.com 的API根本没有配置CORS,它可能不会响应 Access-Control-Allow-* 头,或者甚至会返回404/500错误,因为它不理解 OPTIONS 方法。
    • 即使 bank.com 配置了CORS,它也极不可能允许 malicious.com (Origin) 发送 DELETE 请求,或者允许 X-Confirm-Delete 这样的自定义头。
  3. 浏览器判断预检失败: 浏览器发现 bank.com 的预检响应不符合要求(例如,没有 Access-Control-Allow-Origin 允许 malicious.com,或者不允许 DELETE 方法),因此会立即阻止实际的 DELETE 请求发送。

结果: bank.com 的服务器从未收到那个危险的 DELETE 请求,用户的数据也因此得到了保护。

所以,预检请求的核心价值在于:它将跨域请求的“决策权”从浏览器“传递”给了服务器,但却是在实际请求可能产生副作用之前。 浏览器扮演了一个“中间人”的角色,它在执行潜在危险操作前,先征求服务器的意见,如果服务器不同意,浏览器就直接拒绝执行,从而保护了服务器免受潜在的未授权和非预期操作。

4.2 区分请求的副作用

简单请求(GET, HEAD, POSTContent-Type 限制)被认为是相对安全的,因为:

  • GETHEAD 是幂等的,不应有副作用。
  • POST 请求虽然有副作用,但其 Content-Type 限制为传统表单提交类型,这些类型在CORS出现之前就已经被广泛使用,且服务器对它们有普遍的预期处理方式。恶意网站通过这些方式进行攻击(如CSRF)通常需要其他辅助手段,而CORS在此基础上提供了额外的防护。

复杂请求(PUT, DELETE 或自定义头,application/json 等)则被认为具有更高风险,因为它们:

  • 明确设计用于修改或删除资源。
  • 自定义头可能触发服务器的特定逻辑。
  • application/json 等现代 Content-Type 在CORS出现之前并不像传统表单提交那样普遍用于跨域请求,因此服务器可能没有针对它们进行充分的跨域安全考虑。

预检请求正是针对这些高风险操作提供了一层额外的保障。

4.3 明确的服务器授权

预检请求强制服务器明确地声明它允许哪些跨域操作。这使得CORS成为一个“选择加入”的安全机制。如果服务器没有响应预检请求,或者响应不正确,那么浏览器就不会发送实际请求。这避免了服务器在不知情的情况下,因为浏览器的“善意”而执行了不安全的跨域操作。

5. 代码示例:服务器端如何处理预检请求

为了更好地理解预检请求,我们来看一些服务器端的代码示例。无论是使用Node.js、Python、Java还是其他语言,处理CORS的核心逻辑都是在响应头中设置正确的 Access-Control-* 字段。

5.1 Node.js (Express) 示例

在 Express 框架中,我们通常会使用 cors 中间件来简化CORS的处理。

安装 cors 中间件:

npm install cors

使用 cors 中间件:

const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000;

// 配置 CORS 选项
const corsOptions = {
    origin: 'http://app.example.com', // 只允许这个源访问
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', // 允许的方法
    allowedHeaders: 'Content-Type,Authorization,X-Custom-Header', // 允许的自定义头
    credentials: true, // 允许发送Cookie等凭证
    optionsSuccessStatus: 204 // 对于OPTIONS请求返回204状态码
};

// 全局启用 CORS 中间件,会处理所有路由的 OPTIONS 请求
app.use(cors(corsOptions));

app.use(express.json()); // 用于解析 application/json 请求体

// 简单 GET 请求
app.get('/api/data', (req, res) => {
    res.json({ message: 'This is some data (GET).' });
});

// 复杂 PUT 请求
app.put('/api/resource/:id', (req, res) => {
    // 假设这里执行了更新资源的操作
    console.log(`Resource ${req.params.id} updated with data:`, req.body);
    console.log('Custom Header:', req.headers['x-custom-header']);
    res.json({ message: `Resource ${req.params.id} updated successfully.` });
});

// 自定义处理 OPTIONS 请求的路由 (如果不用 cors 中间件)
// app.options('/api/resource/:id', (req, res) => {
//     res.header('Access-Control-Allow-Origin', 'http://app.example.com');
//     res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
//     res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Custom-Header');
//     res.header('Access-Control-Max-Age', '86400'); // 缓存1天
//     res.sendStatus(204); // 返回 204 No Content
// });

app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
});

在这个例子中,cors 中间件会自动处理 OPTIONS 请求,并根据 corsOptions 配置返回相应的 Access-Control-* 头。当一个 PUT 请求(复杂请求)从 http://app.example.com 发送过来时,cors 中间件会拦截 OPTIONS 请求,并根据配置返回正确的响应,然后浏览器才会发送实际的 PUT 请求。

5.2 Python (Flask) 示例

在 Flask 框架中,我们可以使用 Flask-CORS 扩展。

安装 Flask-CORS

pip install Flask-CORS

使用 Flask-CORS

from flask import Flask, jsonify, request
from flask_cors import CORS

app = Flask(__name__)

# 配置 CORS
# 方法一:全局启用CORS
# CORS(app, resources={r"/api/*": {"origins": "http://app.example.com"}})

# 方法二:更细粒度的控制,可以指定方法和头
CORS(app, resources={r"/api/*": {
    "origins": "http://app.example.com",
    "methods": ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE", "OPTIONS"],
    "allow_headers": ["Content-Type", "Authorization", "X-Custom-Header"],
    "supports_credentials": True,
    "max_age": 86400
}})

@app.route('/api/data', methods=['GET'])
def get_data():
    return jsonify({"message": "This is some data (GET)."})

@app.route('/api/resource/<int:resource_id>', methods=['PUT'])
def update_resource(resource_id):
    # 假设这里执行了更新资源的操作
    data = request.json
    print(f"Resource {resource_id} updated with data: {data}")
    print(f"Custom Header: {request.headers.get('X-Custom-Header')}")
    return jsonify({"message": f"Resource {resource_id} updated successfully."})

if __name__ == '__main__':
    app.run(debug=True, port=3000)

Flask-CORS 扩展同样能够自动识别并处理 OPTIONS 预检请求,根据配置添加正确的响应头。

5.3 客户端 JavaScript (Fetch API) 示例

这是客户端如何发起一个会触发预检请求的复杂请求。

// 假设当前运行在 http://app.example.com
const apiUrl = 'http://api.example.com/api/resource/123';
const authToken = 'some_jwt_token'; // 模拟一个认证token

fetch(apiUrl, {
    method: 'PUT',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${authToken}`, // 自定义头
        'X-Custom-Header': 'Hello-CORS' // 另一个自定义头
    },
    body: JSON.stringify({ name: 'New Name', value: 123 })
})
.then(response => {
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
})
.then(data => {
    console.log('Update successful:', data);
})
.catch(error => {
    console.error('There was a problem with the fetch operation:', error);
});

当这段代码在 http://app.example.com 运行时,它将首先向 http://api.example.com/api/resource/123 发送一个 OPTIONS 预检请求,携带 Origin: http://app.example.comAccess-Control-Request-Method: PUTAccess-Control-Request-Headers: Content-Type, Authorization, X-Custom-Header。只有当服务器响应允许这些条件后,浏览器才会发送实际的 PUT 请求。

6. Access-Control-Max-Age:预检请求的缓存

预检请求虽然提供了强大的安全性保障,但它也引入了一个额外的网络往返(round-trip latency)。对于频繁进行的复杂请求,每次都发送 OPTIONS 请求会增加延迟。为了优化这一点,CORS 引入了 Access-Control-Max-Age 响应头。

Access-Control-Max-Age 头告诉浏览器,预检请求的响应可以被缓存多长时间(以秒为单位)。在缓存有效期内,对于同一URL和相同的 Access-Control-Request-MethodAccess-Control-Request-Headers 组合,浏览器将不再发送重复的 OPTIONS 请求,而是直接发送实际请求。

示例:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Access-Control-Max-Age: 86400 # 缓存1天 (24 * 60 * 60 秒)

影响和注意事项:

  • 性能提升: 对于频繁的复杂请求,Access-Control-Max-Age 可以显著减少网络请求次数,提高应用性能。
  • 开发调试: 在开发阶段,如果CORS策略经常变动,过大的 Access-Control-Max-Age 值可能会导致浏览器缓存旧的预检结果,从而出现CORS问题。此时,可以将该值设置得小一些(例如 0 或几秒),或者在浏览器开发者工具中禁用缓存。
  • 浏览器限制: 浏览器对 Access-Control-Max-Age 的最大值有自己的限制,例如 Chrome 和 Firefox 通常会将最大值限制在2小时左右 (7200秒),Safari 限制在5分钟 (300秒)。即使服务器设置了更大的值,浏览器也会使用其内部的最大限制。

7. 常见问题与排查

CORS 错误是前端开发中非常常见的问题。了解预检请求机制有助于我们更有效地进行排查。

  1. “No ‘Access-Control-Allow-Origin’ header is present on the requested resource.”

    • 原因: 服务器在响应中没有包含 Access-Control-Allow-Origin 头,或者其值不匹配请求的 Origin
    • 排查:
      • 检查服务器CORS配置,确保 Origin 被正确列出。
      • 确认服务器是否正确处理了 OPTIONS 请求(对于复杂请求)。
      • 注意生产环境和开发环境的 Origin 是否一致。
  2. “Preflight request failed with status code 403/404/500.”

    • 原因: 服务器没有正确处理 OPTIONS 请求,或者拒绝了预检请求。
    • 排查:
      • 确认服务器路由是否配置了 OPTIONS 方法的处理器。许多框架或中间件需要显式地允许 OPTIONS 方法。
      • 检查服务器日志,看 OPTIONS 请求是否到达服务器,以及服务器返回了什么错误。
      • 确保服务器在 OPTIONS 响应中设置了正确的 Access-Control-Allow-MethodsAccess-Control-Allow-Headers
  3. “Request header field X-Custom-Header is not allowed by Access-Control-Allow-Headers.”

    • 原因: 实际请求中使用了自定义头,但服务器在预检响应的 Access-Control-Allow-Headers 中没有列出该头。
    • 排查:
      • 在服务器CORS配置中,将所有预期的自定义头添加到 Access-Control-Allow-Headers 列表中。
  4. *Access-Control-Allow-Credentials 与 `` 的冲突**

    • 原因:Access-Control-Allow-Credentials 设置为 true 时,Access-Control-Allow-Origin 不能设置为 *。它必须是一个具体的域名。
    • 排查:
      • 如果需要发送 Cookie 等凭证,请将 Access-Control-Allow-Origin 设置为具体的 Origin,而不是 *

8. 总结

预检请求,作为CORS机制中复杂请求的前置步骤,是浏览器为了保障Web安全而采取的深思熟虑的设计。它通过在实际操作前向服务器进行“安全咨询”,有效地保护了那些可能没有CORS意识的遗留服务器,避免了在未经服务器明确授权的情况下,执行可能具有破坏性或非预期的跨域操作。理解预检请求的必要性、工作流程以及服务器端的处理方式,是每一位Web开发者掌握CORS、构建健壮安全应用的基石。它不仅仅是一个技术细节,更体现了Web安全设计中平衡开放性与风险控制的智慧。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注