一句话概括
跨域是指不同源(协议/域名/端口任一不同)的页面或 API 之间进行资源访问时,受到浏览器的同源策略限制。前端开发者需要掌握至少 4 种跨域方案(CORS、JSONP、代理、postMessage),其中 CORS 是事实标准,其他方案用于特定场景的补充。
背景与意义
为什么要有同源策略?
如果没有同源策略,恶意网站可以:
1
2
3
4
| // 假设你在 bank.com 登录了(有 Cookie)
// 你在浏览恶意网站 evil.com,它执行:
fetch('https://bank.com/api/transfer?amount=10000&to=evil')
.then(() => alert('转账成功!'))
|
浏览器允许 evil.com 发起请求到 bank.com(所以 CSRF 攻击存在),但不允许 evil.com 读取 bank.com 的响应——这就是同源策略的核心:可以发送,不能读取。
什么算是”同源”?
1
2
3
| https://example.com:443/page1
└─────┬─────┘ └─┬──┘ └┬┘ └───┬──┘
协议 域名 端口 路径
|
| URL | 是否同源 | 原因 |
|---|
https://example.com/page2 | ✅ 同源 | 协议/域名/端口相同 |
http://example.com/page | ❌ 不同源 | 协议不同(https vs http) |
https://api.example.com | ❌ 不同源 | 域名不同(子域名不算同源) |
https://example.com:8080 | ❌ 不同源 | 端口不同 |
https://www.example.com | ❌ 不同源 | 域名不同(www 算子域名) |
面试高频信号
跨域是前端面试必考,几乎 100% 会出现:
- “什么是同源策略?为什么要有它?”
- “跨域有哪些解决方案?”
- “CORS 的简单请求和预检请求有什么区别?”
- “CORS 相关的 HTTP 头部有哪些?”
概念与定义
方案概览
| 方案 | 场景 | 限制 | 推荐度 |
|---|
| CORS | 跨域 API 调用 | 需要服务端配合 | ⭐⭐⭐(首选) |
| JSONP | 只支持 GET 请求 | 老旧 API/不想改服务端 | ⭐⭐(过渡方案) |
| 反向代理 | 开发/同域部署 | 需要代理服务器 | ⭐⭐⭐⭐(工程化首选) |
| postMessage | 跨域 iframe 通信 | 需要双方配合 | ⭐⭐(iframe 场景) |
| WebSocket | 实时通信 | 协议不同 | ⭐⭐⭐(实时场景) |
| document.domain | 相同主域的子域 | 已废弃 | ❌ |
CORS(Cross-Origin Resource Sharing)
CORS 是 W3C 标准,通过 HTTP 头部让服务端声明”允许哪些来源访问我”。
1
2
3
4
5
6
| # 服务端响应添加的头部
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
|
简单请求 vs 预检请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // ✅ 简单请求:直接发送(满足全部条件)
// 1. 方法:GET / HEAD / POST
// 2. 头部:只包含 CORS 安全头部(Accept / Accept-Language / Content-Language / Content-Type)
// 3. Content-Type 只能是:text/plain / multipart/form-data / application/x-www-form-urlencoded
fetch('https://api.example.com/data')
// → 直接发出请求,响应中检查 Access-Control-Allow-Origin
// ❌ 非简单请求:先发 OPTIONS 预检
fetch('https://api.example.com/data', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' }, // 非简单 Content-Type
credentials: 'include' // 携带 Cookie
})
// → 浏览器先发一个 OPTIONS 请求(预检)
// → 预检通过后才发真正的 PUT 请求
|
最小示例
CORS 后端配置示例
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Node.js / Express
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://your-frontend.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
// 预检请求直接返回 200
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Nginx 反向代理配置(解决跨域)
location /api/ {
proxy_pass https://api-backend.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 添加 CORS 头部
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
|
核心知识点拆解
知识点 1:CORS 完整头部体系
请求头(客户端自动添加):
1
2
3
| Origin: https://example.com # 告诉服务端"我是谁"
Access-Control-Request-Method: PUT # 预检时表明要用的 HTTP 方法
Access-Control-Request-Headers: content-type, authorization # 预检时表明要用的自定义头部
|
响应头(服务端必须添加):
1
2
3
4
5
6
7
8
9
10
| # 必选
Access-Control-Allow-Origin: https://example.com
# 或通配符:*(但不能与 credentials: true 同时使用)
# 可选(根据场景)
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true # 允许携带 Cookie
Access-Control-Max-Age: 86400 # 预检结果缓存 24 小时
Access-Control-Expose-Headers: X-Total-Count # 允许前端读取的额外响应头
|
知识点 2:JSONP 的原理与局限
1
2
3
4
5
6
7
8
9
10
11
| <!-- JSONP:利用 <script> 标签没有跨域限制 -->
<script>
function handleData(data) {
console.log('收到数据:', data);
}
</script>
<script src="https://api.example.com/user?callback=handleData"></script>
<!-- 服务端返回: -->
<script>handleData({ name: '张三', age: 25 })</script>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 封装 JSONP
function jsonp(url, params, callbackName) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
const callback = `jsonp_${Date.now()}`;
window[callback] = (data) => {
delete window[callback];
document.body.removeChild(script);
resolve(data);
};
const query = new URLSearchParams({ ...params, [callbackName]: callback });
script.src = `${url}?${query}`;
script.onerror = reject;
document.body.appendChild(script);
});
}
// 使用
jsonp('https://api.example.com/user', { id: 1 }, 'callback')
.then(data => console.log(data));
|
JSONP 的局限:
- ❌ 只支持 GET 请求
- ❌ 没有错误处理机制(只能检测 script 加载失败)
- ❌ 安全性低(可被 XSS 攻击利用)
- ❌ 无法设置自定义头部
知识点 3:代理方案
开发环境(Webpack/Vite):
1
2
3
4
5
6
7
8
9
10
11
12
| // Vite 配置
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api-backend.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
|
生产环境(Nginx 反向代理):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # 前端:https://example.com
# API:https://api.example.com
# 通过 Nginx 将 /api 转发到后端,前端请求自己域下的 /api
server {
listen 443 ssl;
server_name example.com;
location / {
root /var/www/frontend;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass https://api-backend.com/;
proxy_set_header Host $host;
}
}
|
原理:浏览器 → 同域请求 → Nginx → 代理到后端 → 浏览器从 Nginx 接收响应。浏览器始终认为请求的是同域资源,没有跨域问题。
知识点 4:postMessage(iframe 通信)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // 父页面:发送消息给 iframe
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage({
type: 'SET_TOKEN',
payload: 'abc123'
}, 'https://iframe-origin.com'); // 指定接收源
// iframe 页面:监听消息
window.addEventListener('message', (event) => {
// ⚠️ 重要:一定要验证来源
if (event.origin !== 'https://parent-origin.com') return;
if (event.data.type === 'SET_TOKEN') {
localStorage.setItem('token', event.data.payload);
// 回复父页面
event.source.postMessage({ type: 'TOKEN_RECEIVED' }, event.origin);
}
});
|
实战案例
案例一:生产环境完整 CORS 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Node.js Express 带白名单
const ALLOWED_ORIGINS = [
'https://example.com',
'https://www.example.com',
'https://admin.example.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
res.setHeader('Access-Control-Max-Age', '86400');
}
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
|
案例二:使用 cors 中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const cors = require('cors');
// 简单用法
app.use(cors({ origin: 'https://example.com' }));
// 动态白名单
const whitelist = ['https://site1.com', 'https://site2.com'];
app.use(cors({
origin: (origin, callback) => {
if (whitelist.includes(origin) || !origin) { // !origin 允许服务端间调用
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
|
案例三:前端错误处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| async function fetchWithCors(url, options = {}) {
try {
const response = await fetch(url, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
// 跨域错误或网络错误
console.error('CORS 或网络错误:', url);
// 可以尝试 fallback 方案
}
throw error;
}
}
|
底层原理
浏览器处理 CORS 的完整流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 浏览器发起 fetch('https://api.b.com/data')
│
├─ 检查是否为简单请求?
│ ├─ 是 → 直接发送请求
│ └─ 否 → 先发 OPTIONS 预检请求
│ │
│ ├─ OPTIONS 返回 2xx + 正确头部 → 发送真正请求
│ └─ OPTIONS 失败 → 抛出跨域错误
│
├─ 发送真正请求
│
└─ 检查响应头 Access-Control-Allow-Origin
├─ 匹配请求 Origin → ✅ 正常接收数据
└─ 不匹配或缺失 → ❌ 拦截响应,控制台报 CORS 错误
(请求已经到达服务器并返回了,但浏览器不交给前端代码)
|
💡 关键理解:CORS 错误意味着:
- 请求已经发出去了(服务器收到了请求,也返回了响应)
- 但浏览器拦截了响应,不让 JS 代码读取
- 这不是”请求失败”,而是”读取失败”
高频面试题解析
Q1:跨域有哪些解决方案?请逐一说明。
参考答案:
- CORS (首选):服务端设置
Access-Control-Allow-Origin 头部,支持所有 HTTP 方法 - JSONP:利用
<script> 无跨域限制,只支持 GET,安全性稍差 - 反向代理(Nginx / Vite Proxy):开发和生产环境都可用
- postMessage:iframe 跨域通信
- WebSocket:不受同源策略限制,适合实时通信
- document.domain:(已废弃)只能用于相同主域的子域之间
Q2:简单请求和预检请求的区别?
参考答案: 简单请求不需要预检,直接发送请求,响应中检查 Access-Control-Allow-Origin。 非简单请求先发 OPTIONS 预检,通过后才发真正请求。
触发预检的条件:
- 使用 PUT / DELETE / PATCH 等方法
Content-Type 为 application/json 等非简单类型- 使用了自定义请求头(Authorization 等)
Q3:CORS 相关的 HTTP 头部有哪些?
参考答案: 请求头:Origin 预检请求头:Access-Control-Request-Method、Access-Control-Request-Headers 响应头:Access-Control-Allow-Origin(必选)、Allow-Methods、Allow-Headers、Allow-Credentials、Max-Age、Expose-Headers
Q4:跨域请求带 Cookie 需要注意什么?
参考答案: 前端设置 credentials: 'include'(fetch)或 withCredentials: true(XHR) 服务端必须设置:
Access-Control-Allow-Origin 不能是 *(必须是具体源)Access-Control-Allow-Credentials: true
总结与扩展
核心要点
- 同源策略 = 协议 + 域名 + 端口三者完全一致
- CORS 是标准方案,绝大多数场景都可以用
- JSONP 已过时,只在维护老旧系统时使用
- 反向代理 是工程化最佳实践(开发 Vite Proxy / 生产 Nginx)
- CORS 不是”服务器拒绝了请求”,而是”浏览器拦截了响应”
延伸学习方向
- CORS 与 CSRF:CORS 是跨域访问控制,CSRF 是跨站请求伪造攻击
- OAuth 2.0 中的跨域授权流程
- CORS 与 Subresource Integrity(SRI) 的安全配合
- crossorigin 属性:
<script crossorigin> 与 CORS 的关系
相关主题