跨域问题是 Web 前端开发中最常见的拦路虎之一。你在本地开发环境调接口调得好好的,一上线就报错:
Access to fetch at 'https://api.example.com/data' from origin 'https://mysite.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present这篇文章从同源策略的根源讲起,逐步深入到 CORS 的完整工作机制和实战配置。
一、为什么会跨域?
浏览器有个同源策略(Same-Origin Policy):只有协议 + 域名 + 端口完全相同的页面,才能共享 DOM、读取 Cookie 和发送跨域请求。
https://mysite.com/page.html
↓
https://mysite.com/api ✅ 同源(协议、域名、端口都一致)
http://mysite.com/api ❌ 协议不同(https vs http)
https://api.mysite.com ❌ 域名不同
https://mysite.com:3000 ❌ 端口不同同源策略保护了你:恶意网站不能通过脚本偷取你在银行页面的数据——因为跨域请求被浏览器拦截。
但实际开发中跨域是刚需:
| 场景 | 跨域原因 |
|---|---|
| 前后端分离 | 前端 localhost:3000,后端 localhost:8080 |
| 调用第三方 API | 自己网站请求 api.weixin.qq.com |
| CDN 资源 | 自己的页面引用 cdn.example.com 上的字体/样式 |
| 微前端 | 多个子应用来自不同域名 |
二、CORS 是什么?
CORS(Cross-Origin Resource Sharing,跨域资源共享) 是 W3C 标准,它允许服务器声明哪些源可以访问它的资源。
核心流程就一句话:浏览器发请求,服务器在响应头里告诉浏览器"这个源我允许",浏览器才把响应交给前端代码。
浏览器和服务器之间的 CORS 处理分两种情况:
三、简单请求(Simple Request)
满足以下所有条件的请求属于简单请求:
- 方法:
GET、HEAD、POST之一 - 请求头:只有
Accept、Accept-Language、Content-Language、Content-Type(且值只能是application/x-www-form-urlencoded、multipart/form-data、text/plain) - 不使用
ReadableStream - 不使用
XMLHttpRequest.upload事件监听
浏览器直接发送请求,通过响应头判断是否放行:
请求头(浏览器自动添加):
Origin: https://mysite.com响应头(服务端必须返回):
Access-Control-Allow-Origin: https://mysite.com前端代码:
fetch('https://api.example.com/data')
.then(res => res.json())
.then(console.log);如果服务器返回了 Access-Control-Allow-Origin: * 或者匹配的源,浏览器就把响应交给前端;否则报 CORS 错误。
四、预检请求(Preflight Request)
当请求不满足"简单请求"条件时(比如使用 PUT/DELETE、Content-Type: application/json、携带自定义头),浏览器会先发一个 OPTIONS 预检请求,询问服务器是否允许实际请求。
OPTIONS /data HTTP/1.1
Origin: https://mysite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization服务器回应:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400预检通过后,浏览器才发送真正的 PUT 请求。
预检请求相当于浏览器替你的代码先去问一声:"老大,我能发这个请求吗?" 服务器说"行"才真的发出。
五、CORS 响应头详解
Access-Control-Allow-Origin
必需。指定允许访问的源。
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Origin: * # 开发环境常用,生产慎用注意:当设置 * 时,不能同时使用 Access-Control-Allow-Credentials: true。
Access-Control-Allow-Methods
预检请求中必需。列出允许的 HTTP 方法。
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCHAccess-Control-Allow-Headers
预检请求中必需。列出允许的自定义请求头。
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-WithAccess-Control-Allow-Credentials
是否需要携带 Cookie/Authorization 等凭证信息。
Access-Control-Allow-Credentials: true前端配合:
// fetch
fetch('https://api.example.com/user', {
credentials: 'include', // 或 'same-origin'
});
// axios
axios.get('/api/user', { withCredentials: true });当
credentials: true时,Access-Control-Allow-Origin不能是*,必须明确指定具体的源。
Access-Control-Expose-Headers
默认情况下,浏览器只暴露 6 个响应头给前端:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。要访问其他响应头(如 X-Total-Count、X-RateLimit-Remaining),需要用这个头声明。
Access-Control-Expose-Headers: X-Total-Count, X-RateLimit-Remainingfetch('/api/list')
.then(res => {
const total = res.headers.get('X-Total-Count');
console.log('总数:', total);
});Access-Control-Max-Age
指定预检请求的结果可以被缓存多久(秒),避免频繁 OPTIONS 请求。
Access-Control-Max-Age: 86400Chrome 上限是 86400 秒(24 小时),Firefox 是 86400 秒。
六、实战配置
Node.js(Express)
const express = require('express');
const app = express();
// CORS 中间件
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowed = ['https://mysite.com', 'https://admin.mysite.com'];
if (allowed.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');
// 预检请求直接 204 返回
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
app.get('/api/data', (req, res) => {
res.json({ message: 'CORS 配置成功' });
});
app.listen(3000);也可以直接用 cors 包:
const cors = require('cors');
app.use(cors({
origin: ['https://mysite.com', 'https://admin.mysite.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400,
}));PHP
<?php
header('Access-Control-Allow-Origin: https://mysite.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
// 预检请求直接结束
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// 正常业务逻辑
echo json_encode(['message' => 'CORS 配置成功']);如果是统一入口(如 Laravel/ThinkPHP),CORS 中间件是更好的选择。
Nginx(反向代理层解决)
如果你能控制 Nginx 配置,可以在代理层统一处理 CORS:
server {
listen 443 ssl;
server_name api.example.com;
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://mysite.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://mysite.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'X-Total-Count' always;
proxy_pass http://backend:3000;
}
}七、开发环境下的 CORS 最佳实践
方案一:Vite 代理(推荐)
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
});请求 /api/data → 被 Vite 代理 → 转发到 https://api.example.com/data → 浏览器看到的是同源请求,彻底消灭跨域。
方案二:Webpack DevServer 代理
// webpack.config.js
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
},
},
}方案三:自己写 CORS 代理
对于调试第三方 API,可以用临时代理:
// Node.js 代理脚本
const http = require('http');
const { createProxyMiddleware } = require('http-proxy-middleware');
const proxy = createProxyMiddleware({
target: 'https://api.thirdparty.com',
changeOrigin: true,
on: {
proxyRes: (proxyRes) => {
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
},
},
});
http.createServer((req, res) => {
if (req.url.startsWith('/proxy/')) {
req.url = req.url.replace('/proxy/', '/');
return proxy(req, res);
}
res.writeHead(404);
res.end();
}).listen(3000);八、常见 CORS 报错及排查
| 错误信息 | 原因 | 解决 |
|---|---|---|
No 'Access-Control-Allow-Origin' header is present |
响应头缺失 | 服务端加上对应头 |
Response to preflight request doesn't pass access control check |
预检失败 | 确认 OPTIONS 请求返回正确头 |
The value of the 'Access-Control-Allow-Origin' header contains multiple values |
重复设置该头 | 检查是否有多个中间件/反向代理同时设置 |
The request's credentials mode is 'include' but ... not '*'. |
credentials 与 * 冲突 |
将 * 改为具体的源 |
Request header field X-Custom is not allowed by Access-Control-Allow-Headers |
自定义头未声明 | 在预检响应中 Allow-Headers 添加该头 |
排查步骤:
- 打开浏览器 DevTools → Network 标签
- 找到 CORS 失败的请求
- 检查请求头中的
Origin - 检查响应头中是否有
Access-Control-Allow-Origin - 如果是 OPTIONS 请求,检查
Allow-Methods和Allow-Headers - 确认 Nginx/CDN 没有覆盖掉服务端的 CORS 头
九、CORS 与 CDN/边缘网关
如果你的网站使用了 CDN(如 EdgeOne、Cloudflare),CORS 配置可能需要额外注意。
场景:源站服务器上配好了 CORS 头,但 CDN 缓存了不带 CORS 头的旧响应。
解决:确保 CDN 缓存策略考虑 Origin 头,或对 OPTIONS 请求不做缓存。
以 EdgeOne 为例,缓存规则中设置:
- 请求方法为
OPTIONS时:不缓存 - 或者根据
Origin头做缓存区分
十、总结
CORS 的核心就是几句话:
- 浏览器发请求时自动带上
Origin - 服务器在响应头里告诉浏览器允许哪些源
- 浏览器检查响应头,决定是否把数据交给前端 JavaScript
- 预检请求是浏览器替实际请求先探路
- 开发环境用代理(Vite/Webpack)最省心,生产环境后端/网关配 CORS
记住:CORS 不是安全问题,是浏览器的一项保护机制。跨域问题是它工作时遇到了配置不完整,而不是它出了故障。正确配置 CORS 后,前后端分离、微服务架构、第三方 API 集成都可以顺畅运行。