一、跨域的本质:同源策略为何存在?
1. 什么是同源策略?
浏览器的核心安全机制,要求协议、域名、端口三者完全一致才视为「同源」,否则即为跨域。例如:
- 禁止:http://localhost:3000 → https://localhost:3000(协议不同)
- 禁止:http://localhost:3000 → http://127.0.0.1:3000(域名不同)
- 禁止:http://localhost:3000 → http://localhost:8080(端口不同)
2. 同源策略保护什么?
- 防止 DOM 劫持(如 iframe 跨域操作)
- 防止 Cookie/Storage 被非法读取
- 防止 XMLHttpRequest/Fetch 跨域请求数据泄露
二、常见跨域场景(开发中必遇)
- 前后端分离:前端(3000 端口)→ 后端 API(8080 端口)
- 第三方接口调用:前端 → 高德地图 / 微信支付 API
- 跨域资源加载:CDN 引入脚本、图片、字体(部分场景触发跨域)
三、7 种主流跨域解决方案(含实战代码)
方案 1:CORS(跨域资源共享)- 推荐首选
原理:后端通过响应头告知浏览器「允许该域名跨域访问」,支持 GET/POST/PUT/DELETE 等所有请求方法,兼容所有现代浏览器。
实战配置(分语言)
- Node.js(Express):
// 安装cors中间件:npm i cors
const express = require('express');
const cors = require('cors');
const app = express();
// 全局允许跨域(开发环境)
app.use(cors({
origin: 'http://localhost:3000', // 允许的前端域名(生产环境指定具体域名,不要用*)
credentials: true, // 允许携带Cookie
methods: ['GET', 'POST', 'PUT', 'DELETE'], // 允许的请求方法
allowedHeaders: ['Content-Type', 'Authorization'] // 允许的请求头
}));
app.get('/api/data', (req, res) => {
res.json({ message: '跨域请求成功' });
});
app.listen(8080);
- Java(Spring Boot):
// 全局配置类
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 所有接口
.allowedOrigins("http://localhost:3000") // 允许的前端域名
.allowedMethods("*") // 允许所有方法
.allowedHeaders("*") // 允许所有请求头
.allowCredentials(true) // 允许携带Cookie
.maxAge(3600); // 预检请求缓存时间(1小时)
}
}
- 前端请求(Axios):
axios({
url: 'http://localhost:8080/api/data',
method: 'GET',
withCredentials: true // 关键:允许携带Cookie(需与后端credentials:true配套)
}).then(res => console.log(res.data));
核心细节:
- 简单请求(GET/POST,请求头无自定义字段):直接发送请求,后端返回 Access-Control-Allow-Origin 即可。
- 预检请求(PUT/DELETE、自定义头、Content-Type: application/json):浏览器先发送 OPTIONS 请求校验,通过后再发送真实请求。
方案 2:JSONP - 兼容老浏览器(仅支持 GET)
原理:利用<script>标签不受同源策略限制的特性,通过回调函数获取跨域数据。
实战代码:
- 后端(Node.js):
app.get('/api/jsonp', (req, res) => {
const callback = req.query.callback; // 前端传入的回调函数名
const data = JSON.stringify({ message: 'JSONP跨域成功' });
res.send(`${callback}(${data})`); // 执行回调函数
});
- 前端:
function handleJsonp(data) {
console.log(data); // 接收跨域数据
}
// 动态创建script标签
const script = document.createElement('script');
script.src = 'http://localhost:8080/api/jsonp?callback=handleJsonp';
document.body.appendChild(script);
优缺点:
- 优点:兼容 IE6+,实现简单。
- 缺点:仅支持 GET 请求,存在 XSS 安全风险,无法捕获错误。
方案 3:代理服务器 - 开发 / 生产环境通用
原理:通过同源的代理服务器转发跨域请求(浏览器→代理→目标服务器),因为代理与前端同源,不存在跨域问题。
场景 1:开发环境(Webpack/Vite)
- Webpack 配置(vue.config.js):
module.exports = {
devServer: {
proxy: {
'/api': { // 匹配所有以/api开头的请求
target: 'http://localhost:8080', // 目标后端地址
changeOrigin: true, // 允许跨域
pathRewrite: { '^/api': '' } // 去除请求路径中的/api前缀
}
}
}
};
- 前端请求:
// 此时请求的是代理服务器(同源),由代理转发到8080端口
axios.get('/api/data').then(res => console.log(res.data));
场景 2:生产环境(Nginx 代理)
server {
listen 80;
server_name localhost;
# 代理前端页面
location / {
root /usr/share/nginx/html;
index index.html;
}
# 代理后端API
location /api/ {
proxy_pass http://localhost:8080/; # 转发到后端地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
方案 4:document.domain - 主域名相同场景
原理 :当两个页面主域名相同(如a.test.com和b.test.com),可通过设置document.domain = 'test.com'实现跨域通信。
实战代码:
- 父页面(a.test.com):
<iframe src="http://b.test.com/child.html" id="iframe"></iframe>
<script>
document.domain = 'test.com'; // 设置主域名
window.onload = function() {
const iframe = document.getElementById('iframe');
console.log(iframe.contentWindow.data); // 访问子页面数据
};
</script>
- 子页面(b.test.com):
document.domain = 'test.com'; // 与父页面保持一致
window.data = '子页面跨域数据';
方案 5:postMessage - 跨窗口 /iframe 通信
原理:HTML5 新增 API,允许不同域名的窗口 /iframe 之间安全传递数据。
实战代码:
- 发送方(a.com):
const iframe = document.getElementById('iframe');
// 触发时机:确保iframe加载完成
iframe.onload = function() {
// 目标窗口、数据、允许的域名
iframe.contentWindow.postMessage('Hello 跨域窗口', 'http://b.com');
};
- 接收方(b.com):
window.addEventListener('message', (e) => {
// 验证发送方域名(关键:防止恶意攻击)
if (e.origin === 'http://a.com') {
console.log(e.data); // 接收数据:Hello 跨域窗口
// 回复数据
e.source.postMessage('收到消息', e.origin);
}
});
方案 6:WebSocket - 全双工跨域通信
原理:WebSocket 协议不受同源策略限制,建立连接后可双向实时通信(适用于聊天、实时数据推送)。
实战代码:
- 后端(Node.js + ws):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
console.log('收到客户端消息:', message);
ws.send('WebSocket跨域通信成功'); // 发送消息给客户端
});
});
- 前端:
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
ws.send('前端请求连接');
};
ws.onmessage = (e) => {
console.log('收到服务端消息:', e.data);
};
方案 7:Access-Control-Allow-Origin: * - 简单场景应急
适用场景:公开 API(无需携带 Cookie、无自定义头),直接在后端设置响应头:
// Node.js示例
app.get('/api/public', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*'); // 允许所有域名跨域
res.json({ message: '公开API跨域成功' });
});
⚠️ 注意:设置*时,后端不能开启credentials: true(Cookie 传递),否则浏览器会报错。
四、解决方案选型指南(90% 场景覆盖)
|------------------|---------------------------------|-------------------------|
| 场景 | 推荐方案 | 不推荐方案 |
| 前后端分离(开发 / 生产) | CORS + 代理服务器 | JSONP |
| 兼容 IE6/7 老浏览器 | JSONP | CORS |
| 跨窗口 /iframe 通信 | postMessage | document.domain(仅主域名相同) |
| 实时通信(聊天 / 推送) | WebSocket | 轮询(效率低) |
| 公开 API(无 Cookie) | Access-Control-Allow-Origin: * | - |
| 主域名相同(子域名不同) | document.domain | CORS(略显冗余) |
五、避坑指南(90 分必备细节)
- *CORS 携带 Cookie 时,origin 不能设为 **:必须指定具体域名(如http://localhost:3000),且前后端需同时开启credentials: true。
- 预检请求失败:检查后端是否允许OPTIONS方法,是否配置了Access-Control-Allow-Headers(对应前端自定义头)。
- JSONP XSS 风险:后端需过滤回调函数名(如禁止特殊字符),前端避免使用不可信的第三方 JSONP 接口。
- 代理服务器路径匹配:Webpack 代理的pathRewrite需注意是否去除前缀,避免后端接口 404。
- postMessage 安全风险:接收方必须验证e.origin,防止恶意网站伪造消息。
- CDN 资源跨域:图片 / 字体跨域时,CDN 需设置Access-Control-Allow-Origin,前端<img>标签无需额外配置(浏览器自动处理)。
六、总结
跨域问题的核心是浏览器同源策略,解决方案的本质都是「绕开或合规突破该策略」。实际开发中,CORS + 代理服务器 是最通用、最安全的组合(覆盖 90% 场景),其他方案根据特殊需求选型(如老浏览器用 JSONP、实时通信用 WebSocket)。
记住:跨域的核心是「后端授权」,前端方案仅为辅助,最终必须通过后端配置实现合法跨域(前端绕过同源策略的方案均存在安全风险,不推荐生产环境使用)。