Skip to main content

跨域与解决方案

跨域是因为浏览器的同源策略(协议、域名、端口必须一致)限制了网络请求或 DOM 访问。现代开发中最主流的解决方案是服务端配置 CORS 和本地开发配置 Nginx/Node 代理JSONP 仅支持 GET 且已趋于淘汰,postMessage 则专门用于 iframe 跨域通信。

为什么会有跨域?

同源策略(Same-Origin Policy)是浏览器最核心的安全机制。如果两个 URL 的 协议(Protocol)、域名(Host)、端口(Port) 有任何一个不同,就会被判定为跨域。 限制范围:

  1. 无法读取非同源网页的 Cookie、LocalStorage 和 IndexDB。
  2. 无法获取非同源网页的 DOM(如 iframe 嵌套)。
  3. 向非同源地址发送 AJAX 请求会被浏览器拦截响应(注意:请求其实已经发出去了,服务端也处理了,只是浏览器把返回的结果拦截了不给 JS 用)。

1.CORS (跨域资源共享) - 最主流方案

这是面试最核心的考点。CORS 是 W3C 标准,它允许浏览器向跨源服务器发出 XMLHttpRequest 请求。

核心配置:服务端在响应头中设置 Access-Control-Allow-Origin 即可开启。

在高级面试中,通常会追问 “简单请求”与“非简单请求(预检请求 Preflight)” 的区别:

① 简单请求

  • 满足条件:方法为 GET/HEAD/POST,且 Content-Type 仅限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  • 流程:浏览器直接发出请求,并在请求头带上 Origin 字段。服务器根据 Origin 决定是否在响应头中加上 Access-Control-Allow-Origin

② 非简单请求与 OPTIONS 预检

  • 触发条件:当请求是 PUT/DELETE,或者 Content-Type 是 application/json(极常见!),或者携带了自定义 Header(如 Authorization token)时。
  • 预检(Preflight)机制:在发送真实的业务请求前,浏览器会自动先发一个 OPTIONS 请求去问服务器:“我能不能用 PUT 方法?能不能带 Authorization 头?”
  • 响应:服务器在 OPTIONS 响应中返回 Access-Control-Allow-MethodsAccess-Control-Allow-Headers,浏览器确认没问题后,才会发出真正的业务请求。
  • 优化点:可以通过设置 Access-Control-Max-Age 缓存预检结果,减少一次网络握手开销。

如果跨域请求需要携带 Cookie,必须前后端配合:

// 前端设置是否带cookie
axios.defaults.withCredentials = true;

// 3. 后端设置
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); // 绝不能是 *

面试追问:为什么 Allow-Credentials: true 时,Allow-Origin 绝不能是 *

这是一个经典的 Web 安全设计(防 CSRF 攻击机制)。 我们可以通过一个极端的反面演示来理解: 假设浏览器允许了同时设置 Credentials: trueOrigin: *

  1. 正常操作:你在浏览器登录了 bank.com,浏览器保存了你的银行身份 Cookie。
  2. 钓鱼发生:你手滑点开了一个恶意网站 hacker.com
  3. 恶意代码执行hacker.com 的 JS 代码里偷偷执行了一段 axios.post('http://bank.com/transfer', { amount: 1000 }, { withCredentials: true })
  4. 灾难降临:因为银行 API 的 Origin*(来者不拒),且允许带凭证,浏览器就会乖乖地带上你的银行 Cookie,向银行发送这笔转账请求。银行一看,Cookie 是合法的你,就直接把钱转走了。

正因为如此,W3C 标准直接从底层物理隔绝了这种风险:只要你需要前端跨域携带 Cookie,后端就必须点名道姓地说出允许哪一个域名来访问,绝不能“海纳百川”。


2.Nginx / Node 代理 (实战最常用)

原理:同源策略是浏览器的安全策略,服务器之间是没有跨域限制的。 所以我们可以在前端和真正的后端之间,架设一个和前端同源的代理服务器。前端发请求给代理,代理再转发给真正的后端。

  • 本地开发阶段:通常配置 Webpack/Vite 的 proxy(底层是 http-proxy-middleware Node 层代理)。
  • 生产环境阶段:通常配置 Nginx 的反向代理。
server {
listen 80;
server_name www.frontend.com;

# 凡是带有 /api/ 的请求,全部转发给真正的后端服务器
location /api/ {
proxy_pass http://www.real-backend.com;
}
}

3.JSONP (远古方案)

原理:利用 <script><img> 标签加载资源不受同源策略限制的漏洞。通过动态创建 <script> 标签,让服务端返回一段调用本地 JS 函数的代码,从而把数据传回来。 致命缺点只能发 GET 请求,且容易遭受 XSS 攻击。现代工程中几乎已被 CORS 完全取代。

// 极简版 JSONP 实现原理
function jsonp(url, callbackName) {
return new Promise((resolve) => {
window[callbackName] = function (data) {
resolve(data);
document.body.removeChild(script); // 拿完数据就清理标签
};
let script = document.createElement("script");
script.src = `${url}?callback=${callbackName}`;
document.body.appendChild(script);
});
}

4. postMessage (跨窗口/跨 iframe 通信)

postMessage 是 HTML5 引入的 API,专门用于解决前端页面之间(Window 对象之间)的跨源通讯。

它不仅限于父子 iframe,只要你能拿到目标窗口的 Window 对象引用,就能跨域发消息:

  • 父子 iframeiframe.contentWindowwindow.parent
  • 兄弟 iframe:通过父页面作为中转,或者 window.parent.frames[1]
  • 新开的 Tab 页const newWin = window.open(...)
// a.com 页面 (发送方)
const iframe = document.getElementById("my-iframe");
// 发送数据并指定目标源,增强安全性
iframe.contentWindow.postMessage(
JSON.stringify({ msg: "hello" }),
"http://b.com",
);

// b.com 页面 (接收方)
window.addEventListener("message", function (e) {
// 面试必考坑点:核心防御!务必验证消息来源!
// 如果不加这个 if 判断,任何网页都可以通过 iframe 嵌套你的网页并向你发送恶意指令。
if (e.origin !== "http://a.com") return;
console.log("收到数据:", e.data);
});

5. WebSocket 跨域

WebSocket 是一种双向通信协议,它的请求头格式是以 ws://wss:// 开头。 最重要的是:WebSocket 协议本身并不受同源策略的限制。只要服务器允许,任何域名的前端页面都可以连接到该 WebSocket 服务。