文章

前端跨域方案完全解析

同源策略是"能发不能读"的浏览器安全底线,跨域是前端面试必考题。 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 头把读权限授权给指定源、预检请求守护非简单调用;工程上用反向代理把跨域消弭于同源之内——分清”请求已到、响应被拦”,你才算真正懂了跨域。

本文由作者按照 CC BY 4.0 进行授权