前端 CORS 深度解析
1. 什么是 CORS?
CORS(Cross-Origin Resource Sharing,跨域资源共享)是一种浏览器的安全机制,用于限制从一个源(协议 + 域名 + 端口)加载的脚本去请求另一个源的资源。
它的核心目标是 保护用户数据安全,避免恶意网站随意请求用户隐私数据。
2. 简单请求与非简单请求
在 CORS 中,请求分为两类:
2.1 简单请求(Simple Request)
满足以下所有条件时,才算简单请求:
- 请求方法:只能是
GET
、HEAD
、POST
- 允许的请求头:只能是以下几种(开发者可设置):
Accept
Accept-Language
Content-Language
Content-Type
(但值必须受限,见下条)Range
(且仅允许简单范围值,如bytes=256-
或bytes=127-255
)
- Content-Type 限制:只能是以下三种之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
- XHR 上传限制:如果使用 XMLHttpRequest,不能在
xhr.upload
上注册事件监听器。
如果满足这些条件,浏览器会 直接发请求,不会发 OPTIONS 预检。
2.2 非简单请求(Preflighted Request)
只要不满足上述条件之一,就会被认为是非简单请求,浏览器会先发 OPTIONS 预检请求,确认服务器允许后才发真正的请求。
常见触发预检的场景:
- 使用了
PUT
、DELETE
、PATCH
等方法 - 自定义请求头(如
X-Token
、Authorization
) Content-Type: application/json
- 使用了
xhr.upload.addEventListener
监听上传进度
3. 浏览器自动带的请求头
即使前端没有写任何 header,浏览器也会自动加一些,例如:
accept: */*
accept-encoding: gzip, deflate, br, zstd
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
connection: keep-alive
host: localhost:3000
origin: http://localhost:5173
pragma: no-cache
referer: http://localhost:5173/
sec-ch-ua
(客户端 UA Hints)sec-fetch-*
(安全上下文)user-agent
这些属于 浏览器自动设置的“禁用头”,开发者不能随意改动,也不会导致请求从简单请求变成预检请求。
4. Vue + Vite + Koa 跨域问题
4.1 场景
- 前端:Vite 开发服务器,运行在
http://localhost:5173
- 后端:Koa 服务,运行在
http://localhost:3000
- 请求:
fetch("http://localhost:3000/api")
因为端口不同,构成跨域,请求会被浏览器拦截。
4.2 解决方案
方式 1:Koa 端配置 CORS
安装依赖:
npm install @koa/cors
在 app.js
中使用:
const Koa = require("koa");
const cors = require("@koa/cors");const app = new Koa();app.use(cors({origin: "http://localhost:5173",credentials: true,allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],allowHeaders: ["Content-Type", "Authorization"]
}));app.listen(3000);
方式 2:Vite 配置代理(开发环境推荐)
在 vite.config.js
中:
export default defineConfig({server: {proxy: {"/api": {target: "http://localhost:3000",changeOrigin: true,rewrite: path => path.replace(/^\/api/, "")}}}
});
请求时改成:
fetch("/api/xxx");
这样请求走 5173,同源,Vite 再转发到 3000。
5. 总结
- CORS 是浏览器的安全机制,保护用户免受跨站攻击。
- 简单请求无需预检,非简单请求需要预检(OPTIONS)。
- 浏览器自动加的头(Accept、User-Agent、Origin 等)不会破坏简单请求判定。
- 本地开发常用 代理 或 服务端允许跨域 两种方式解决问题。
- 生产环境更推荐服务端配置 CORS,开发环境推荐前端代理。
6. Vite 代理与 API_BASE_URL 的关系
6.1 为什么请求还是带 http://localhost:3000/api
如果在前端代码里这样写:
fetch("http://localhost:3000/api/login")
就会直连后端 3000 端口,绕过 Vite 代理,自然还是跨域。
因为 Vite 代理只会拦截以 /api
开头的相对路径,不会拦截完整 URL。
6.2 正确用法
-
在开发环境中,只写相对路径:
fetch("/api/login", { ... })
这样才会走 Vite 代理 → 转发到
http://localhost:3000/login
。 -
在生产环境中,可以写成真实后端地址:
fetch("https://your.prod.server/login", { ... })
6.3 环境变量配置
推荐在 Vite 中使用 .env
文件来区分:
.env.development
:
VITE_API_BASE_URL=/api
.env.production
:
VITE_API_BASE_URL=https://your.prod.server
前端代码:
fetch(`${import.meta.env.VITE_API_BASE_URL}/login`, {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify({ username, password })
})
6.4 总结
- 开发环境:
/api
→ 命中代理,转发到后端 3000 端口。 - 生产环境:真实后端地址 → 直接请求后端服务。
- 切记:如果写死
http://localhost:3000/...
,代理不会生效,CORS 错误依旧存在。