跨域与解决方案
跨域是因为浏览器的同源策略(协议、域名、端口必须一致)限制了网络请求或 DOM 访问。现代开发中最主流的解决方案是服务端配置 CORS 和本地开发配置 Nginx/Node 代理。
JSONP仅支持 GET 且已趋于淘汰,postMessage则专门用于 iframe 跨域通信。
为什么会有跨域?
同源策略(Same-Origin Policy)是浏览器最核心的安全机制。如果两个 URL 的 协议(Protocol)、域名(Host)、端口(Port) 有任何一个不同,就会被判定为跨域。 限制范围:
- 无法读取非同源网页的 Cookie、LocalStorage 和 IndexDB。
- 无法获取非同源网页的 DOM(如 iframe 嵌套)。
- 向非同源地址发送 AJAX 请求会被浏览器拦截响应(注意:请求其实已经发出去了,服务端也处理了,只是浏览器把返回的结果拦截了不给 JS 用)。
1.CORS (跨域资源共享) - 最主流方案
这是面试最核心的考点。CORS 是 W3C 标准,它允许浏览器向跨源服务器发出
XMLHttpRequest请求。
核心配置:服务端在响应头中设置 Access-Control-Allow-Origin 即可开启。
在高级面试中,通常会追问 “简单请求”与“非简单请求(预检请求 Preflight)” 的区别:
① 简单请求
- 满足条件:方法为
GET/HEAD/POST,且Content-Type仅限text/plain、multipart/form-data、application/x-www-form-urlencoded。 - 流程:浏览器直接发出请求,并在请求头带上
Origin字段。服务器根据 Origin 决定是否在响应头中加上Access-Control-Allow-Origin。
② 非简单请求与 OPTIONS 预检
- 触发条件:当请求是
PUT/DELETE,或者 Content-Type 是application/json(极常见!),或者携带了自定义 Header(如Authorizationtoken)时。 - 预检(Preflight)机制:在发送真实的业务请求前,浏览器会自动先发一个
OPTIONS请求去问服务器:“我能不能用 PUT 方法?能不能带 Authorization 头?” - 响应:服务器在 OPTIONS 响应中返回
Access-Control-Allow-Methods和Access-Control-Allow-Headers,浏览器确认没问题后,才会发出真正的业务请求。 - 优化点:可以通过设置
Access-Control-Max-Age缓存预检结果,减少一次网络握手开销。
③ 携带 Cookie 跨域
如果跨域请求需要携带 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: true和Origin: *。
- 正常操作:你在浏览器登录了
bank.com,浏览器保存了你的银行身份 Cookie。- 钓鱼发生:你手滑点开了一个恶意网站
hacker.com。- 恶意代码执行:
hacker.com的 JS 代码里偷偷执行了一段axios.post('http://bank.com/transfer', { amount: 1000 }, { withCredentials: true })。- 灾难降临:因为银行 API 的
Origin是*(来者不拒),且允许带凭证,浏览器就会乖乖地带上你的银行 Cookie,向银行发送这笔转账请求。银行一看,Cookie 是合法的你,就直接把钱转走了。正因为如此,W3C 标准直接从底层物理隔绝了这种风险:只要你需要前端跨域携带 Cookie,后端就必须点名道姓地说出允许哪一个域名来访问,绝不能“海纳百川”。
2.Nginx / Node 代理 (实战最常用)
原理:同源策略是浏览器的安全策略,服务器之间是没有跨域限制的。 所以我们可以在前端和真正的后端之间,架设一个和前端同源的代理服务器。前端发请求给代理,代理再转发给真正的后端。
- 本地开发阶段:通常配置 Webpack/Vite 的
proxy(底层是http-proxy-middlewareNode 层代理)。 - 生产环境阶段:通常配置 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 对象引用,就能跨域发消息:
- 父子 iframe:
iframe.contentWindow或window.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 服务。