前端跨域方案完全解析
同源策略是"能发不能读"的浏览器安全底线,跨域是前端面试必考题。 CORS 是事实标准,JSONP/代理/postMessage 是特定场景的补充,预检请求是高频考点。
一句话概括
跨域,指协议、域名、端口任意一个不同的两个源之间,浏览器按”同源策略”限制它们互相读数据。它保护的不是”能不能发请求”,而是”能不能读响应”——这是理解整个跨域问题的总开关。
最该记住的一句:同源策略 = 能发,不能读。恶意站点确实能把请求发到银行接口(所以才有 CSRF),但读不到银行返回的数据,这就断了它偷信息的念想。
解法里 CORS 是 W3C 标准、事实上的正解;JSONP 是上古 GET-only 妥协;反向代理是工程化首选(开发 Vite Proxy、生产 Nginx);postMessage 专治跨域 iframe 通信。面试基本就围着”为什么跨域 + 哪几种方案 + 预检请求”转。
核心知识点
1. 同源策略:能发不能读
判定同源就三个维度,全一样才算同源:
| URL | 是否同源 | 原因 |
|---|---|---|
https://example.com/page2 | ✅ 同源 | 协议/域名/端口全同 |
http://example.com/page | ❌ | 协议不同(http vs https) |
https://api.example.com | ❌ | 子域名不同 |
https://example.com:8080 | ❌ | 端口不同 |
https://www.example.com | ❌ | 域名不同 |
1
2
3
4
5
// 你在 evil.com 登录了 bank.com(带 Cookie)
// evil.com 能发出这个请求:
fetch('https://bank.com/api/transfer?amount=10000&to=evil');
// ❌ 但浏览器拦截响应:JS 读不到任何返回内容
// 这就是同源策略的核心:请求能发出去,响应读不到
2. CORS:正解,靠 HTTP 头授权
CORS 让服务端用响应头声明”我允许哪些源访问我”。浏览器自动在请求里带 Origin,并按响应头决定是否把数据交给 JS。
1
2
3
4
5
6
7
8
9
10
// ✅ 简单请求:方法 GET/HEAD/POST,且头部/Content-Type 都在"安全清单"内
// 满足条件:直接发,浏览器检查响应里的 Access-Control-Allow-Origin
fetch('https://api.example.com/data')
// ❌ 非简单请求:会先发 OPTIONS 预检(下面知识点 3 细说)
fetch('https://api.example.com/data', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' }, // 非简单类型
credentials: 'include' // 带 Cookie
})
服务端该回的头部:
1
2
3
4
5
6
Access-Control-Allow-Origin: https://your-frontend.com # 必填,* 不能和 credentials 同用
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true # 允许带 Cookie
Access-Control-Max-Age: 86400 # 预检结果缓存 24h
Access-Control-Expose-Headers: X-Total-Count # 允许前端读取的额外响应头
1
2
3
4
5
6
7
8
9
10
11
12
13
// Node/Express 带白名单的 CORS 中间件(生产可用)
const ALLOWED = ['https://example.com', 'https://admin.example.com'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED.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');
}
if (req.method === 'OPTIONS') return res.sendStatus(204); // 预检直接放行
next();
});
3. 预检请求(Preflight):最容易被问翻的点
当请求”不够简单”,浏览器先悄悄发一个 OPTIONS 问服务端”我能不能这么发”,通过后才发真正的请求。触发条件:
- 方法不是 GET/HEAD/POST(如 PUT/DELETE/PATCH)
Content-Type不是text/plain、multipart/form-data、application/x-www-form-urlencoded(即application/json必触发)- 带了自定义请求头(如
Authorization) - 带
credentials且跨域
1
2
3
浏览器: OPTIONS /data (带 Access-Control-Request-Method / -Headers)
服务端: 204 + 允许的头
浏览器: 真正的 PUT /data ← 预检过了才发
关键认知:CORS 报错时,请求已经到达服务器并正常返回了,只是浏览器把响应拦下不交给 JS。所以后端日志能看到这次请求,但前端 fetch 拿不到——这不是”请求失败”,是”读取被拒”。
4. 反向代理:工程化首选
开发用 Vite,生产用 Nginx,思路一致:让浏览器始终请求自己同源的地址,由代理服务器转发到真实后端,浏览器根本感知不到跨域。
1
2
3
4
5
6
7
8
9
10
11
12
// vite.config.js —— 开发环境
export default {
server: {
proxy: {
'/api': {
target: 'https://api-backend.com',
changeOrigin: true,
rewrite: p => p.replace(/^\/api/, '')
}
}
}
}
1
2
3
4
5
6
7
8
9
10
# 生产环境 Nginx 反向代理(前端 example.com,后端 api-backend.com)
server {
listen 443 ssl;
server_name example.com;
location /api/ {
proxy_pass https://api-backend.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5. JSONP 与 postMessage:特定场景补充
1
2
3
<!-- JSONP:靠 <script> 没跨域限制,服务端返回 JS 调用 -->
<script src="https://api.example.com/user?callback=handleData"></script>
<!-- 服务端返回:handleData({ name: '张三' }) -->
JSONP 局限很明显:只支持 GET、无错误处理的精细控制、可被 XSS 利用、不能带自定义头。现在基本只在对接老系统时用。
1
2
3
4
5
6
7
8
// postMessage:跨域 iframe 通信
// 父页 → iframe
iframe.contentWindow.postMessage({ type: 'SET_TOKEN', payload: 'abc' }, 'https://iframe.com');
// iframe 监听(务必校验 origin!)
window.addEventListener('message', e => {
if (e.origin !== 'https://parent.com') return; // 不校验来源 = 任意站点都能冒充
if (e.data.type === 'SET_TOKEN') localStorage.setItem('token', e.data.payload);
});
其实你每天都在用
- 调后端接口
fetch('https://api.xxx.com/...'):只要前后端不同域,就靠后端配 CORS 或 Nginx 代理才能拿到数据 - Vite 开发时代理
/api:你本地localhost:5173调localhost:8080不报错,就是 proxy 在兜底 - 带登录态调接口:
credentials: 'include'+ 服务端Allow-Credentials: true,否则 Cookie 不带、接口让你重新登录 - 上传
application/json报 CORS:八成是触发了预检,后端没处理 OPTIONS 或没放行Content-Type - 内嵌第三方页面(如支付 iframe):父子页通信靠
postMessage,而且一定要校验event.origin - 引入 CDN 上的 SDK(如
cdn.jsdelivr.net):<script>天然跨域,所以 JSONP 和<script src>不受同源限制 - Form 提交到别的域:
<form action="https://other.com">也能跨域发,但同样读不到返回值(典型 CSRF 入口)
常见误解(FAQ)
❌ 误区一:”CORS 报错说明请求没发出去 / 后端拒绝了我的请求”
错。CORS 报错时请求已经到了后端、后端也返回了响应,只是浏览器按同源策略拦截了响应、不交给 JS。所以你看到 CORS 红字,后端其实已经执行了那次调用——这在”写操作”上要特别小心(请求可能已生效)。
❌ 误区二:”Access-Control-Allow-Origin: * 最省事,随便用”
带 * 时浏览器禁止携带 Cookie(credentials 失效),且不能和 Allow-Credentials: true 共存。需要带登录态就必须写具体的源,不能用通配符。生产环境更该用白名单动态匹配。
❌ 误区三:”JSONP 比 CORS 兼容性好,应该优先用”
JSONP 是历史妥协,只有 GET、没精细错误处理、易 XSS。现代浏览器和接口早就该上 CORS;JSONP 仅用于无法改后端的老旧系统。新项目别碰。
❌ 误区四:”跨域是后端的事,前端不用懂”
前端至少要懂:什么时候触发预检、为什么带 Cookie 要特殊配置、代理怎么配、postMessage 为什么要校验 origin。不然联调时能卡你一整天,面试也必挂。
一句话总结
跨域的本质是浏览器”能发不能读“的同源策略,CORS 用 HTTP 头把读权限授权给指定源、预检请求守护非简单调用;工程上用反向代理把跨域消弭于同源之内——分清”请求已到、响应被拦”,你才算真正懂了跨域。