JavaScript内核与高级编程之:`JavaScript`的`CORS`预检请求:其在复杂 `HTTP` 请求中的工作原理。

各位听众,老司机们,以及未来的编程大神们,晚上好!我是今晚的“JavaScript深夜食堂”主讲人。今天咱们聊点儿HTTP协议里有点儿绕,但又不得不搞明白的玩意儿:CORS预检请求。这玩意儿就像HTTP请求里的“安检”,专门对付那些可能不太“老实”的跨域请求。

一、啥是CORS,为啥要有预检?

首先,咱得明确CORS (Cross-Origin Resource Sharing) 跨域资源共享,它是一种浏览器安全机制。浏览器为了防止恶意网站搞事情,默认禁止JavaScript脚本发起跨域请求。啥叫跨域?简单来说,就是你当前页面的域名(协议、域名、端口,三者有一个不一样就算跨域)和你要请求的服务器域名不一样。

举个例子:你现在访问的是 http://www.example.com,然后你的JS代码想去请求 http://api.example.net的数据,这就跨域了。

CORS就是用来放宽这个限制,允许一些跨域请求。但是,为了更安全,有些“危险”的跨域请求,浏览器会先发一个“预检请求”探探路,确认服务器允许这次请求,才会真正发送数据。

二、为啥有些请求要预检,有些不用?

关键在于请求的“复杂度”。CORS把HTTP请求分成了“简单请求”和“复杂请求”。

  • 简单请求 (Simple Request): 这种请求比较“乖”,不会对服务器造成太大风险,所以不用预检,直接发就行。

    简单请求必须满足以下所有条件:

    • 请求方法 (Method): 只能是 GET, HEAD, 或者 POST
    • HTTP头 (Headers): 只能包含以下字段:
      • Accept
      • Accept-Language
      • Content-Language
      • Content-Type (但它的值只能是 application/x-www-form-urlencoded, multipart/form-data, 或者 text/plain)
      • DPR
      • Downlink
      • ECT
      • RTT
      • Save-Data
      • Viewport-Width
      • Width
    • POST请求体 (Body): 只能是 application/x-www-form-urlencoded, multipart/form-data, 或者 text/plain 类型。

    如果你的请求满足以上所有条件,恭喜你,它就是个简单请求,直接发,不用预检。

  • 复杂请求 (Preflighted Request): 只要不满足简单请求的条件,就是复杂请求。比如,使用了 PUT, DELETE 等方法,或者自定义了HTTP头,或者Content-Typeapplication/json,那就必须预检。

三、预检请求是咋回事?

预检请求其实就是一个 OPTIONS 请求。浏览器会在真正发送请求之前,先发一个 OPTIONS 请求到服务器,询问服务器是否允许这次跨域请求。

这个 OPTIONS 请求会携带以下重要的头信息:

  • Origin: 表明请求的来源域名。
  • Access-Control-Request-Method: 表明实际请求将使用的HTTP方法,比如 PUTDELETE
  • Access-Control-Request-Headers: 表明实际请求将包含的自定义HTTP头。

服务器收到 OPTIONS 请求后,必须返回一个响应,这个响应也必须包含一些关键的头信息:

  • Access-Control-Allow-Origin: 指定允许哪些域名跨域访问。可以设置为 * 表示允许所有域名,但通常不建议这样做,因为它不安全。
  • Access-Control-Allow-Methods: 指定允许哪些HTTP方法。
  • Access-Control-Allow-Headers: 指定允许哪些自定义HTTP头。
  • Access-Control-Max-Age: 指定预检请求结果的缓存时间(秒)。在这个时间内,浏览器不会再次发送预检请求。

四、代码演示:一个需要预检的请求

咱们来模拟一个需要预检的请求。

<!DOCTYPE html>
<html>
<head>
  <title>CORS Preflight Example</title>
</head>
<body>
  <button id="myButton">Send Request</button>

  <script>
    document.getElementById('myButton').addEventListener('click', function() {
      fetch('http://api.example.net/data', { // 假设这是个跨域地址
        method: 'PUT', // 使用PUT方法,属于复杂请求
        headers: {
          'Content-Type': 'application/json', // 使用application/json,属于复杂请求
          'X-Custom-Header': 'My Value' // 添加自定义头,属于复杂请求
        },
        body: JSON.stringify({ message: 'Hello from the client!' })
      })
      .then(response => response.json())
      .then(data => console.log(data))
      .catch(error => console.error('Error:', error));
    });
  </script>
</body>
</html>

在这个例子中,我们使用了 PUT 方法,Content-Typeapplication/json,并且还添加了自定义头 X-Custom-Header。这些都使得这个请求变成了一个复杂请求,需要预检。

五、服务器端配置:让预检通过

为了让这个请求成功,我们需要在服务器端配置CORS。这里以Node.js + Express为例,演示如何处理预检请求。

const express = require('express');
const cors = require('cors'); // 引入cors中间件
const app = express();
const port = 3000;

// 使用cors中间件,允许跨域请求
// 配置项可以自定义,这里只是一个简单的例子
const corsOptions = {
  origin: 'http://www.example.com', // 允许的域名
  methods: 'PUT, POST, DELETE, GET, OPTIONS', // 允许的HTTP方法
  allowedHeaders: 'Content-Type, Authorization, X-Custom-Header', // 允许的HTTP头
  maxAge: 86400 // 预检结果缓存一天
};

app.use(cors(corsOptions));
app.use(express.json()); // 为了能解析JSON格式的请求体

// 处理OPTIONS请求 (预检请求)
app.options('/data', cors(corsOptions), (req, res) => {
  console.log("收到OPTIONS预检请求");
  res.sendStatus(204); // 成功处理预检请求,返回204 No Content
});

// 处理PUT请求
app.put('/data', (req, res) => {
  console.log('Received PUT request:', req.body);
  res.json({ message: 'PUT request received successfully!' });
});

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

关键点:

  1. cors 中间件: 使用 cors 中间件可以简化CORS配置。
  2. origin origin 必须设置为允许的域名,或者 * (不推荐)。
  3. methods methods 必须包含实际请求将使用的所有HTTP方法。
  4. allowedHeaders allowedHeaders 必须包含实际请求将使用的所有自定义HTTP头。
  5. maxAge maxAge 控制预检结果的缓存时间。
  6. OPTIONS 处理: 必须显式处理 OPTIONS 请求,并返回 204 No Content。因为浏览器发送预检请求期望服务器返回允许的HTTP方法和Header,而不是直接返回资源。
  7. 明确声明Content-Type: 客户端和服务端都要明确声明Content-Type

六、 浏览器DevTools:观察预检请求

打开浏览器的开发者工具 (Network 面板),你就可以看到预检请求和实际请求的流程。

  1. 发起请求: 点击页面上的 “Send Request” 按钮。
  2. 观察 Network 面板: 你会看到两个请求:
    • 一个 OPTIONS 请求 (预检请求)。
    • 一个 PUT 请求 (实际请求)。
  3. 查看响应头: 检查 OPTIONS 请求的响应头,确认包含了 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 等信息。
  4. 查看请求头: 确保实际请求的请求头包含你在代码中设置的自定义头 X-Custom-Header

七、常见问题及解决方案

  • CORS 错误: 如果出现 CORS 错误,检查以下几点:
    • 服务器是否正确配置了 CORS 头信息。
    • Access-Control-Allow-Origin 是否包含了你的域名。
    • Access-Control-Allow-Methods 是否包含了实际请求使用的HTTP方法。
    • Access-Control-Allow-Headers 是否包含了实际请求使用的所有自定义HTTP头。
  • 预检请求失败: 如果预检请求失败,检查服务器是否正确处理了 OPTIONS 请求。
  • 缓存问题: 浏览器可能会缓存预检请求的结果。如果你的CORS配置发生了变化,但浏览器仍然使用旧的缓存,可以尝试清除浏览器缓存,或者设置 Access-Control-Max-Age 为一个较小的值。
  • 代理问题: 如果你在使用代理服务器,确保代理服务器也正确处理了CORS。
  • 后端框架问题:不同的后端框架CORS配置的方法不同,例如Spring Boot、Django等,需要查看对应框架的CORS配置文档。

八、CORS配置的最佳实践

  • *避免使用 `:** 尽量不要将Access-Control-Allow-Origin设置为*`,因为它会允许所有域名跨域访问,存在安全风险。应该明确指定允许的域名。
  • 精细化配置: 尽量只允许必要的HTTP方法和HTTP头。不要允许所有方法和所有头,这会增加安全风险。
  • 使用中间件: 使用 CORS 中间件可以简化CORS配置。
  • 监控 CORS 错误: 监控CORS错误,可以帮助你及时发现和解决CORS问题。

九、总结

CORS预检请求是浏览器为了保证安全而采取的一种机制。理解CORS的工作原理,以及如何正确配置CORS,对于开发Web应用至关重要。希望今天的讲解能帮助大家更好地理解CORS预检请求,并在实际开发中避免踩坑。

特性 简单请求 复杂请求 (需要预检)
HTTP 方法 GET, HEAD, POST 除了 GET, HEAD, POST 之外的任何方法,如 PUT, DELETE 等
Content-Type application/x-www-form-urlencoded, multipart/form-data, text/plain 任何其他类型,如 application/json, application/xml
自定义 HTTP 头 不允许 允许
是否需要预检请求
风险等级 较低 较高
安全性 依赖浏览器的同源策略 通过预检请求进行更严格的控制
适用场景 简单的数据获取或提交,不涉及复杂操作 需要更复杂的数据交互和权限控制的场景
示例场景 获取网页内容,提交简单的表单数据 使用 RESTful API 进行数据更新、删除,上传文件等

好了,今天的“JavaScript深夜食堂”就到这里。希望大家消化一下,下次再见!

发表回复

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