理解 CORS、预检请求 (Preflight) 和跨域

11 天前(已编辑)
/ ,
29

理解 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 错误

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 天

在跨域场景下,如果后端响应头返回 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));

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...