今天在浏览博客时,我发现了一个有趣的问题:“为什么POST请求会发送两次?”之前我从未注意到这个问题。我的第一反应是,是否是因为防抖处理不当导致POST请求被发送两次。😊经过进一步的资料查询,我发现在某些情况下,请求需要进行预检。本文将对“为什么POST请求会发送两次?”进行简单的解释。
1. 在什么场景下一次请求会发起两次请求
在web开发中,一次操作导致两次HTTP请求的常见场景是使用CORS(跨源资源共享)时的预检请求(preflight request)。这种情况下,浏览器在发送实际的请求之前,会先发送一个OPTIONS请求,以确保实际的请求是安全的。例如:
- 场景: 当你尝试从一个域名下的前端应用向另一个域名的服务器发送一个带有自定义头部或对cookie进行操作的POST请求时。
- 第一次请求(预检请求): 使用OPTIONS方法,询问目标服务器是否接受来自原始域的请求以及是否允许具体的HTTP方法和头部。
- 第二次请求(实际请求): 如果预检请求成功(即服务器响应允许),浏览器随后发送实际的POST请求。
简单请求和预检请求区别
简单请求
简单请求满足以下所有条件:
- 使用以下方法之一:GET, POST, 或 HEAD。
- HTTP头部仅限于接受浏览器自动设置的头部,如
Accept
,Accept-Language
,Content-Language
,Content-Type
等。 - 如果设置了
Content-Type
头部,只能是以下三种值之一:text/plain
,multipart/form-data
, 或application/x-www-form-urlencoded
。 - 请求中不包含任何监听DOM事件的事件处理器,不设置
ReadEventSource
,Fetch
等API。
当这些条件被满足时,浏览器直接发起请求至服务器,不进行CORS预检。
预检请求
如果请求不满足简单请求的条件,浏览器会先发起一个预检请求,该请求使用OPTIONS方法,询问服务器是否允许来自该源的请求具有某些特定的HTTP方法和头部。预检请求的目的是保证安全,防止不受欢迎的跨源请求影响服务器数据。预检请求主要包含以下头部:
Access-Control-Request-Method
: 告诉服务器实际请求将使用的HTTP方法。Access-Control-Request-Headers
: 列出实际请求中将会额外设置的HTTP头部。
区别总结
- 触发条件:简单请求不触发预检,而复杂请求由于其使用的HTTP方法或自定义头部的缘故,需要预检。
- 请求方法:简单请求使用GET、POST或HEAD方法;预检请求使用OPTIONS方法。
- 安全性:预检请求提供了一种安全机制,防止未经许可的跨源请求对服务器造成影响。
这样的设计能够确保在数据被实际发送之前,跨源请求已经获得了必要的校验,增强了网络应用的安全性。
2. 浏览器同源策略与服务器无同源策略的原因
- 浏览器同源策略: 主要是为了保护用户信息安全,防止恶意网站读取另一个网站的数据。例如,如果没有同源策略,一个开放的标签页可能会试图访问另一个标签页的银行账户数据。
- 服务器无同源策略: 服务器处理数据的方式不同,它们设计为接收并响应来自任何来源的请求。服务器的安全策略通常更加灵活和可配置,例如通过CORS设置。
3. 服务器配置CORS
Nginx 配置 CORS
在Nginx中配置CORS需要在服务器的配置文件中设置相应的HTTP头部。例如:
location / {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
}
Node.js (使用 Koa 或 Egg)
在Koa或Egg框架中,通常使用中间件来简化CORS的配置。例如,使用koa2-cors
:
const Koa = require('koa');
const cors = require('koa2-cors');
const app = new Koa();
app.use(cors({
origin: '*',
allowMethods: ['GET', 'POST', 'DELETE', 'PUT', 'OPTIONS']
}));
app.listen(3000);
4. 为什么本地使用Webpack进行开发时,可以跨域访问线上接口
在使用Webpack进行开发时,Webpack Dev Server提供了一个代理功能,可以配置代理转发API请求到另一个服务器。这样做的好处是,你的开发环境的域(localhost:8080)发出的请求被Webpack Dev Server接收,并转发到另一个域,从而绕过了浏览器的同源策略限制。配置示例如下:
// webpack.config.js
module.exports = {
// ...
devServer: {
proxy: {
'/api': {
target: 'http://example.com',
changeOrigin: true,
pathRewrite: {'^/api' : ''}
}
}
}
};