理解 CORS、预检请求 (Preflight) 和跨域
这篇主要来聊一聊前端常见的跨域问题,以及后端如何处理 CORS 和预检请求 (Preflight)。
浏览器在什么情况下会发生跨域
浏览器通过“同源策略”限制不同源之间的资源交互,以保护用户隐私和安全。其中源由三个部分组成:协议、域名 和 端口。只有这三者同时满足才是同源。否则,就是跨域,向服务端发送请求时会触发浏览器的跨域限制,报以下错误:
Access to fetch at 'https://server.suemor.com/api/posts' from origin 'https://suemor.com' CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
具体看下方 4 个例子。
跨域
以下是三种跨域情况:
不同协议
https://suemor.com
http://suemor.com/api/posts
不同域名
https://suemor.com
https://server.suemor.com/api/posts
//tips: 下方这个也是跨域
http://127.0.0.1:3000 -> http://localhost:3000/api/posts
不同端口
http://localhost:3000
http://localhost:5050/api/posts
同源
下方这个是同源,没有跨域问题。
https://suemor.com
https://suemor.com/api/posts
解决跨域
跨域问题通常在服务端解决,通过配置反向代理或修改后端代码。
跨域请求分为简单请求和复杂请求:
简单请求
对于同时满足以下三个条件的即为简单请求,服务器只需返回正确的 CORS 头,即 Access-Control-Allow-Origin:
- 请求方法:GET、POST 或 HEAD。
- Content-Type:限于 application/x-www-form-urlencoded、multipart/form-data 或 text/plain。
- 不包含自定义头:如 Authorization。
看以下 Express 示例:
// 假设 web 端位于 http://localhost:3000
// 假设 server 端位于 http://localhost:5050
// web
fetch("http://localhost:5000/api/posts", { method: "GET" });
// server
app.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000"); // 或者 res.setHeader("Access-Control-Allow-Origin", "");
next();
});
复杂请求
符合以下任意一条,即为复杂请求:
- 使用 PUT、DELETE 等方法。
- Content-Type: application/json。
- 包含自定义请求头(如 Authorization)。
复杂请求比较特殊,浏览器会先发送一个 OPTIONS 方法的预检请求(Preflight),检查服务器是否允许该跨域请求。如果不允许,则直接抛出 CORS 错误,不再发送实际请求。

CORS 错误
因此,我们需要单独处理这个预检请求(Preflight):
// 假设 web 端位于 http://localhost:3000
// 假设 server 端位于 http://localhost:5050
// web
fetch("http://localhost:5050/api/data", {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token",
},
body: JSON.stringify({ data: "example" }),
});
// server
app.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
res.header("Access-Control-Allow-Methods", "PUT");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.status(200).end();
return;
}
next();
});
设置 Access-Control-Max-Age
复杂请求在无缓存或缓存失效时会发送两次请求:Preflight(OPTIONS)和实际请求,这会增加网络开销。为此,服务器可以通过设置 Access-Control-Max-Age 响应头来控制浏览器缓存预检结果的时长。这个头字段的值表示缓存的有效期(以秒为单位)。在缓存有效期内,浏览器会复用之前的预检结果,跳过对相同接口的 Preflight 请求,从而提升性能。
res.header('Access-Control-Max-Age', '86400'); // 缓存 1 天
支持跨域 Cookie 的配置
在跨域场景下,如果后端响应头返回 Set-Cookie,默认不会生效,因为设置 Cookie 需要额外配置以绕过浏览器的安全限制。核心是启用 Access-Control-Allow-Credentials 并明确指定 Access-Control-Allow-Origin。以下是一个 Express 示例:
app.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000"); // 一定要指定具体地址,不能为 *
res.setHeader("Access-Control-Allow-Credentials", "true"); //添加这个
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");// 一定要指定具体地址,不能为 *
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "86400");
res.status(200).end();
return;
}
next();
});
app.post("/api/posts", (req: Request, res: Response) => {
res.cookie("sessionId", "123456789", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "none",
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ success: true, message: "Cookie set" });
});
注意这里 Access-Control-Allow-Origin 一定要指定具体的地址,不能设置为 Access-Control-Allow-Origin: *
,否则 Cookie 无效。
前端如果使用 fetch 调用,则一定要加上 credentials: "include"
否则无法设置 Cookie。如果是 axios 则加上 withCredentials: true
。
//fetch
fetch("http://localhost:5050/api/posts", {
method: "POST",
credentials: "include", // 允许携带和接收 Cookie
}).then((res) => res.json());
//axios
axios({
url: "http://localhost:5050/api/posts",
method: "POST",
withCredentials: true, // 允许携带和接收 Cookie
})
.then((res) => res.data)
.catch((err) => console.error(err));