各位听众,老司机们,以及未来的编程大神们,晚上好!我是今晚的“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
类型。
如果你的请求满足以上所有条件,恭喜你,它就是个简单请求,直接发,不用预检。
- 请求方法 (Method): 只能是
-
复杂请求 (Preflighted Request): 只要不满足简单请求的条件,就是复杂请求。比如,使用了
PUT
,DELETE
等方法,或者自定义了HTTP头,或者Content-Type
是application/json
,那就必须预检。
三、预检请求是咋回事?
预检请求其实就是一个 OPTIONS
请求。浏览器会在真正发送请求之前,先发一个 OPTIONS
请求到服务器,询问服务器是否允许这次跨域请求。
这个 OPTIONS
请求会携带以下重要的头信息:
Origin
: 表明请求的来源域名。Access-Control-Request-Method
: 表明实际请求将使用的HTTP方法,比如PUT
或DELETE
。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-Type
是 application/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}`);
});
关键点:
cors
中间件: 使用cors
中间件可以简化CORS配置。origin
:origin
必须设置为允许的域名,或者*
(不推荐)。methods
:methods
必须包含实际请求将使用的所有HTTP方法。allowedHeaders
:allowedHeaders
必须包含实际请求将使用的所有自定义HTTP头。maxAge
:maxAge
控制预检结果的缓存时间。OPTIONS
处理: 必须显式处理OPTIONS
请求,并返回204 No Content
。因为浏览器发送预检请求期望服务器返回允许的HTTP方法和Header,而不是直接返回资源。- 明确声明
Content-Type
: 客户端和服务端都要明确声明Content-Type
。
六、 浏览器DevTools:观察预检请求
打开浏览器的开发者工具 (Network 面板),你就可以看到预检请求和实际请求的流程。
- 发起请求: 点击页面上的 “Send Request” 按钮。
- 观察 Network 面板: 你会看到两个请求:
- 一个
OPTIONS
请求 (预检请求)。 - 一个
PUT
请求 (实际请求)。
- 一个
- 查看响应头: 检查
OPTIONS
请求的响应头,确认包含了Access-Control-Allow-Origin
,Access-Control-Allow-Methods
,Access-Control-Allow-Headers
等信息。 - 查看请求头: 确保实际请求的请求头包含你在代码中设置的自定义头
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深夜食堂”就到这里。希望大家消化一下,下次再见!