预检请求(Preflight Request)是由浏览器发起的一种特定的 HTTP 请求,用于在实际发送跨源请求之前验证服务器是否允许该跨源请求。预检请求是 CORS(跨源资源共享)机制的一部分。当你尝试从一个域发送请求到另一个域时,浏览器需要保证安全性,因此会首先使用预检请求来询问目标域对该请求的安全限制。
什么是 CORS
CORS(跨源资源共享,Cross-Origin Resource Sharing)是一种安全功能,它允许网页上的代码安全地访问另一个域下的资源。在没有 CORS 的情况下,浏览器出于安全考虑遵循同源策略(Same-Origin Policy),即默认情况下,网页上的 JavaScript 只能从与该网页相同的域中加载和执行资源。CORS 通过添加特定的 HTTP 头,允许服务器指示任何域的代码都可访问其资源。
CORS 的工作原理
在一个 HTTP 请求中,可以分为简单请求和非简单请求,首先我们了解一下什么是简单请求。
简单请求
简单请求是指满足特定标准的请求,这些请求可以直接发出,无需进行 CORS 预检(preflight)流程。简单请求的标准主要涉及请求的方法(method)、头部(headers)和内容类型(Content-Type)。
简单请求只能使用以下三种 HTTP 方法之一:
-
GET:用于请求数据。
-
POST:用于发送数据到服务器。
-
HEAD:用于获取资源的元数据,如响应头。
对于请求头,简单请求可以使用的 HTTP 头部非常有限,只包括一下几种:
-
Accept:告诉服务器客户端能接受哪些媒体类型。例如,Accept: text/html 表示客户端可以接收 HTML 文件。
-
Accept-Language:告诉服务器客户端能接受的语言列表。例如,Accept-Language: en-US, en;q=0.5 表示首选美国英语,但如果不可用,则接受其他类型的英语。
-
Content-Language:告诉服务器请求体中的内容使用的语言。例如,Content-Language: de-DE 表示内容使用德国的德语。
-
Content-Type:这是一个特殊的头部,只有以下三种值被视为简单请求:
-
application/x-www-form-urlencoded:表单默认的内容类型,提交的数据按键值对排列,值被编码在 URI 中。
-
multipart/form-data:用于
<input type="file">
,即上传文件。 -
text/plain:数据以纯文本形式发送,不进行编码。
-
-
Last-Event-ID:此请求头部与服务器发送事件(Server-Sent Events, SSE)相关,用于指定上一个接收的事件的 ID,使得服务端可以从断点继续发送事件。
-
DPR (Device Pixel Ratio):此请求头部告诉服务器设备的像素比例,通常用于响应图片或其他资源的请求,以便服务器可以发送适合设备显示的资源。
-
Downlink:此请求头部描述了用户设备的下行速度估计,以兆位每秒(Mbps)为单位。它可以帮助服务器根据用户当前的网络条件调整响应。
-
Save-Data:如果此请求头部为 on,表明用户愿意为了节省数据而接受更低质量的资源。
-
Viewport-Width:此请求头部表示用户设备的视窗宽度,服务器可以根据这个信息发送最适合的资源,特别是图片或布局。
-
Width:此请求头部是在请求某个资源(如图片)时指定的,用于告诉服务器所请求图片的目标显示宽度。
除此之外,请求不能使用 XMLHttpRequest.upload 对象注册任何事件监听器和不能使用 ReadableStream 对象。
对于简单请求,浏览器直接发出请求,并在请求的 Origin 头部标明请求来源。服务器根据这个来源以及自身的 CORS 策略决定是否接受这个请求。如果接受,它需要在响应中包括 Access-Control-Allow-Origin 头部。浏览器会根据这个头部决定是否将响应暴露给发起请求的前端代码。
非简单请求
除了简单请求,剩下的都是非简单请求。非简单请求在发送实际数据之前,会使用 OPTIONS 方法先发送一个预检请求,询问服务器是否允许来自某源的请求。
预检请求的 HTTP 头中包含以下几个字段:
-
Origin:表示请求来自哪个源。
-
Access-Control-Request-Method:HTTP 方法。
-
Access-Control-Request-Headers:额外的头部字段。
服务器需要响应这些预检请求,并在响应中明确哪些方法、头部和源是被允许的。如果服务器允许,那么浏览器将发送实际请求;如果服务器不允许,浏览器将拒绝发送实际请求。
我们在掘金上面删除一个沸点,并通过 network 来查看他的网络请求:
在上面的请求中,请求体和响应体都有携带了 Access-Control-Request-Method、Access-Control-Request-Headers 等 HTTP 头部信息,而在响应体中返回了 access-control-allow-origin、access-control-max-age 等 HTTP 首部字段。
如果预检成功,返回的 204 状态码,没有内容返回的同时表示成功,浏览器可以发送删除沸点的请求了。
为什么本地使用 Webpack 进行 dev 开发时,不需要服务器端配置 CORS 的情况下访问到线上接口?
在使用 Webpack 进行本地开发时,经常会遇到需要从本地应用访问线上接口的场景。即使线上服务器没有显式配置 CORS(跨源资源共享),本地开发环境依然可以成功调用这些接口。这主要得益于 Webpack 开发服务器(通常是 webpack-dev-server)提供的代理功能。
Webpack Dev Server 可以配置一个代理(Proxy),该代理能将特定的 API 请求从本地开发服务器转发到指定的线上服务器。这个过程中,代理服务器作为中间人,将请求从 localhost(或其他自定义的本地域名)转发到线上 API。
浏览器的同源策略(SOP)阻止从一个源加载的文档或脚本与另一个源的资源进行交互。但是,如果使用 Webpack Dev Server 的代理功能,请求实际上是从本地开发环境发出,经过 Webpack Dev Server 转发到目标 API。因为这个 HTTP 请求首先发到同源的本地开发服务器,浏览器只认为这是一个本地请求,并不触发 CORS 预检请求。
如何在 NodeJs 中配置 CORS
在 NestJS 中配置 CORS 非常简单,NestJS 提供了内置的支持来处理跨源资源共享(CORS)问题。这可以通过在启动 HTTP 服务时直接在 NestJS 的应用设置中配置 CORS 选项来实现。
启用默认的 CORS 配置
在最简单的场景中,你可以启用默认的 CORS 配置,这允许任何源进行跨源请求:
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors(); // 启用CORS,使用默认配置
await app.listen(3000);
}
bootstrap();
在这个配置中,enableCors()方法调用时没有参数,表示接受所有默认的 CORS 设置(例如允许任何域进行跨源请求)。
使用自定义 CORS 配置
如果你需要更精细地控制 CORS 相关的配置,例如指定允许的源、方法、HTTP 头等,你可以向 enableCors()方法提供一个配置对象:
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: "https://www.baidu.com", // 只允许来自https://example.com的请求
methods: "GET,POST", // 只允许GET和POST请求方法
allowedHeaders: "Content-Type,Authorization", // 明确允许的HTTP头
credentials: true, // 支持发送cookies
});
await app.listen(3000);
}
bootstrap();
这个配置提供了更多控制,例如只允许特定的源和 HTTP 方法。
动态确定 CORS 配置
如果你需要根据请求动态确定是否允许 CORS,例如基于请求的某些特征或数据库中的配置,你可以传递一个函数到 origin 属性:
ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: (origin, callback) => {
const allowedOrigins = [
"https://www.baidu.com",
"https://www.bilibili.com/",
];
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("哎哟你干嘛~"), false);
}
},
methods: "GET,POST",
allowedHeaders: "Content-Type,Authorization",
credentials: true,
});
await app.listen(3000);
}
bootstrap();
这里的 origin 属性是一个函数,它接收请求的源作为参数,并通过一个回调函数确定是否允许请求。
我们还可以使用 cors 中间来配置 CORS。
总结
预检请求使用 OPTIONS 方法,其目的是检查实际请求是否安全可接受。预检请求的响应中,服务器可以指明哪些源、哪些 HTTP 方法和头信息字段是可以接受的。如果预检请求失败,主请求不会被发出。这个机制帮助提高了网站的安全性,防止了不被允许的跨源请求可能造成的问题。
最后分享两个我的两个开源项目,它们分别是:
这两个项目都会一直维护的,如果你想参与或者交流学习,可以加我微信 yunmz777 如果你也喜欢,欢迎 star 🚗🚗🚗