跨域解决方案
浏览器的同源策略是 Web 安全的基石,但在前后端分离架构下,跨域请求成为常见需求。本文详细介绍各种跨域解决方案的原理、配置与实践
概述
什么是同源策略
同源策略(Same-Origin Policy)是浏览器最核心的安全机制,它规定:协议(Protocol)、域名(Domain)、端口(Port)必须完全相同,才属于"同源"。
┌─────────────────────────────────────────────────────────────┐
│ 同源判断示例 │
├─────────────────────────────────────────────────────────────┤
│ 当前页面: https://www.example.com:443/page.html │
├─────────────────────────────────────────────────────────────┤
│ URL │ 同源 │ 原因 │
├───────────────────────────────────────────┼──────┼───────────┤
│ https://www.example.com/api/data │ ✓ │ 完全相同 │
│ http://www.example.com/api/data │ ✗ │ 协议不同 │
│ https://api.example.com/data │ ✗ │ 域名不同 │
│ https://www.example.com:8080/api/data │ ✗ │ 端口不同 │
│ https://example.com/api/data │ ✗ │ 子域名不同│
└─────────────────────────────────────────────────────────────┘跨域限制范围
同源策略主要限制以下行为:
| 受限行为 | 说明 | 示例 |
|---|---|---|
| Ajax 请求 | 无法读取跨域响应内容 | fetch('http://other.com/api') |
| Cookie | 无法读取跨域 Cookie | document.cookie |
| DOM 操作 | 无法操作跨域页面 DOM | iframe.contentDocument |
| LocalStorage | 无法访问跨域存储 | localStorage.getItem() |
| IndexedDB | 无法访问跨域数据库 | indexedDB.open() |
为什么需要跨域解决方案
在现代 Web 开发中,跨域需求非常普遍:
┌─────────────────────────────────────────────────────────────┐
│ 典型跨域场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 前端应用 │ │ 后端 API │ │
│ │ localhost: │ ────▶ │ api.example │ 开发环境跨域 │
│ │ 3000 │ │ .com │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 主站 │ │ 子系统 │ │
│ │ www.a.com │ ────▶ │ app.a.com │ 微前端架构 │
│ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 第三方服务 │ │ 开放平台 │ │
│ │ client.com │ ────▶ │ open.api.com │ 公共 API 调用 │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘一、CORS(跨域资源共享)
CORS(Cross-Origin Resource Sharing)是 W3C 标准,是目前最主流的跨域解决方案。它通过在服务器设置响应头来告知浏览器允许跨域访问。
基本原理
┌─────────────┐ ┌─────────────┐
│ 浏览器 │ │ 服务器 │
│ │ │ │
│ 发送请求 │ ① Request │ │
│ Origin: │ ─────────────────────────────▶│ │
│ http://a.. │ │ 检查 Origin │
│ │ │ │
│ │ ② Response │ 设置响应头 │
│ │ ◀─────────────────────────────│ │
│ │ Access-Control-Allow-Origin │ │
│ │ Access-Control-Allow-Methods│ │
│ │ │ │
│ 允许跨域 │ │ │
│ 访问响应 │ │ │
└─────────────┘ └─────────────┘简单请求
满足以下所有条件的请求为简单请求,不会触发预检:
请求方法:
GETPOSTHEAD
请求头部:
AcceptAccept-LanguageContent-LanguageContent-Type(仅限application/x-www-form-urlencoded、multipart/form-data、text/plain)
// 简单请求示例 - GET 请求
fetch('http://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
// 简单请求示例 - POST 表单数据
fetch('http://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'name=Alice&age=25'
});服务器端处理:
// Express 示例
app.get('/api/data', (req, res) => {
// 允许所有来源
res.header('Access-Control-Allow-Origin', '*');
res.json({ data: 'success' });
});
// 更安全的配置 - 指定来源
app.get('/api/data', (req, res) => {
const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
res.json({ data: 'success' });
});预检请求
不满足简单请求条件时,浏览器会先发送 OPTIONS 请求进行预检:
┌─────────────┐ ┌─────────────┐
│ 浏览器 │ │ 服务器 │
│ │ │ │
│ │ ① OPTIONS 预检请求 │ │
│ │ ─────────────────────────────▶│ │
│ │ Origin: http://example.com │ 检查权限 │
│ │ Access-Control-Request- │ │
│ │ Method: PUT │ │
│ │ │ │
│ │ ② 预检响应 │ │
│ │ ◀─────────────────────────────│ │
│ │ Access-Control-Allow-Origin │ │
│ │ Access-Control-Allow-Methods│ │
│ │ Access-Control-Max-Age │ │
│ │ │ │
│ │ ③ 实际请求 │ │
│ │ ─────────────────────────────▶│ │
│ │ │ │
│ │ ④ 实际响应 │ │
│ │ ◀─────────────────────────────│ │
└─────────────┘ └─────────────┘// 触发预检的请求示例
fetch('http://api.example.com/data', {
method: 'PUT', // 非简单方法
headers: {
'Content-Type': 'application/json', // 非简单 Content-Type
'Authorization': 'Bearer token' // 自定义头部
},
body: JSON.stringify({ name: 'Alice' })
});服务器端处理预检:
// Express 处理预检请求
app.options('/api/*', (req, res) => {
res.header('Access-Control-Allow-Origin', 'http://example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
res.header('Access-Control-Max-Age', '86400'); // 预检结果缓存 24 小时
res.sendStatus(204);
});
// 或者使用 cors 中间件
const cors = require('cors');
app.use(cors({
origin: 'http://example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
}));携带身份凭证
默认情况下,跨域请求不携带 Cookie。如需携带,需同时配置客户端和服务器:
// 客户端配置
// Fetch API
fetch('http://api.example.com/data', {
credentials: 'include' // 包含 Cookie
});
// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
// Axios
axios.get('http://api.example.com/data', {
withCredentials: true
});// 服务器端配置
app.get('/api/data', (req, res) => {
// 不能使用 *,必须指定具体来源
res.header('Access-Control-Allow-Origin', 'http://example.com');
res.header('Access-Control-Allow-Credentials', 'true');
res.json({ data: 'success' });
});
// 使用 cors 中间件
app.use(cors({
origin: 'http://example.com', // 不能为 '*'
credentials: true
}));响应头详解
| 响应头 | 说明 | 示例值 |
|---|---|---|
Access-Control-Allow-Origin | 允许的来源 | * 或 http://example.com |
Access-Control-Allow-Methods | 允许的方法 | GET, POST, PUT |
Access-Control-Allow-Headers | 允许的请求头 | Content-Type, Authorization |
Access-Control-Allow-Credentials | 是否允许携带凭证 | true |
Access-Control-Max-Age | 预检结果缓存时间(秒) | 86400 |
Access-Control-Expose-Headers | 暴露给客户端的响应头 | X-Custom-Header |
// Access-Control-Expose-Headers 示例
app.get('/api/data', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Size');
res.header('X-Total-Count', '100');
res.header('X-Page-Size', '10');
res.json({ data: [] });
});
// 客户端获取自定义响应头
fetch('http://api.example.com/data')
.then(response => {
console.log(response.headers.get('X-Total-Count')); // 100
console.log(response.headers.get('X-Page-Size')); // 10
});服务器配置示例
Express:
const express = require('express');
const cors = require('cors');
const app = express();
// 基础 CORS 配置
app.use(cors());
// 自定义 CORS 配置
const corsOptions = {
origin: function (origin, callback) {
const whitelist = ['http://example.com', 'http://localhost:3000'];
if (!origin || whitelist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));Koa:
const Koa = require('koa');
const cors = require('@koa/cors');
const app = new Koa();
app.use(cors({
origin: 'http://example.com',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));Nginx:
server {
location /api {
# 允许的来源
add_header 'Access-Control-Allow-Origin' '$http_origin';
# 允许的方法
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
# 允许的头部
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
# 允许携带凭证
add_header 'Access-Control-Allow-Credentials' 'true';
# 预检缓存时间
add_header 'Access-Control-Max-Age' 86400;
# 处理预检请求
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://backend;
}
}错误处理与调试
常见错误信息:
Access to fetch at 'http://api.example.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.调试技巧:
// 1. 检查请求是否触发预检
fetch('http://api.example.com/data', {
method: 'GET',
headers: {
'X-Custom-Header': 'value' // 触发预检
}
}).catch(error => console.error('CORS Error:', error));
// 2. 使用 Chrome DevTools 查看详情
// Network → 请求 → Headers → Response Headers
// 3. 服务器端调试
app.use((req, res, next) => {
console.log('Request Origin:', req.headers.origin);
console.log('Request Method:', req.method);
console.log('Request Headers:', req.headers);
next();
});错误处理最佳实践:
// 客户端封装
async function fetchWithCORS(url, options = {}) {
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error.message.includes('CORS')) {
console.error('跨域错误,请检查服务器 CORS 配置');
}
throw error;
}
}二、JSONP
JSONP(JSON with Padding)是早期跨域解决方案,利用 <script> 标签不受同源策略限制的特性。
基本原理
┌─────────────┐ ┌─────────────┐
│ 浏览器 │ │ 服务器 │
│ │ │ │
│ 创建script │ ① <script src="url? │ │
│ 标签 │ callback=func"> │ │
│ │ ─────────────────────────────▶│ │
│ │ │ 执行回调 │
│ │ ② func({...data...}) │ 包装数据 │
│ │ ◀─────────────────────────────│ │
│ │ │ │
│ 执行回调 │ │ │
│ 获取数据 │ │ │
└─────────────┘ └─────────────┘实现方式
客户端实现:
/**
* JSONP 请求封装
* @param {string} url - 请求地址
* @param {object} params - 请求参数
* @param {string} callbackParam - 回调参数名,默认 callback
* @returns {Promise} 返回 Promise
*/
function jsonp(url, params = {}, callbackParam = 'callback') {
return new Promise((resolve, reject) => {
// 生成唯一回调函数名
const callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;
// 创建 script 元素
const script = document.createElement('script');
// 将回调函数挂载到全局,供服务器调用
window[callbackName] = function(data) {
resolve(data);
// 请求完成后清理
delete window[callbackName];
document.body.removeChild(script);
};
// 构建 URL:合并 params 和 callback 参数
const query = Object.entries({ ...params, [callbackParam]: callbackName })
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
const fullUrl = url + (url.includes('?') ? '&' : '?') + query;
// 处理加载错误
script.onerror = function() {
reject(new Error('JSONP 请求失败'));
delete window[callbackName];
document.body.removeChild(script);
};
script.src = fullUrl;
document.body.appendChild(script);
});
}
// 使用示例
jsonp('http://api.example.com/data', { id: 123 })
.then(data => console.log('成功:', data))
.catch(error => console.error('失败:', error));服务器端实现:
// Express
app.get('/api/data', (req, res) => {
const callback = req.query.callback;
const data = { name: 'Alice', age: 25 };
if (callback) {
// JSONP 响应
res.type('text/javascript');
res.send(`${callback}(${JSON.stringify(data)})`);
} else {
// 普通 JSON 响应
res.json(data);
}
});优缺点分析
| 优点 | 缺点 |
|---|---|
| 兼容性好,支持 IE6+ | 只支持 GET 请求 |
| 实现简单,无需服务器额外配置 | 存在安全风险(XSS) |
| 不受同源策略限制 | 无法获取响应状态码 |
| 需要服务器配合修改 |
安全注意事项
// 安全风险示例 - 恶意代码注入
// 服务器返回: callback({data: '</script><script>alert("XSS")</script>'});
// 安全实践 1: 验证回调函数名
app.get('/api/data', (req, res) => {
const callback = req.query.callback;
// 只允许合法的函数名
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(callback)) {
return res.status(400).send('Invalid callback name');
}
const data = { name: 'Alice' };
res.type('text/javascript');
res.send(`${callback}(${JSON.stringify(data)})`);
});
// 安全实践 2: 限制来源
app.get('/api/data', (req, res) => {
const referer = req.headers.referer || '';
const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
if (!allowedOrigins.some(origin => referer.startsWith(origin))) {
return res.status(403).send('Forbidden');
}
// ... 正常响应
});三、代理服务器
代理服务器是常用的跨域解决方案,通过同源服务器转发请求,避免浏览器跨域限制。
工作原理
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 浏览器 │ │ 代理服务器 │ │ 目标服务器 │
│ │ │ │ │ │
│ 发送请求 │ ───▶ │ 接收请求 │ ───▶ │ 处理请求 │
│ 同源请求 │ │ 转发请求 │ │ 返回响应 │
│ │ ◀─── │ 返回响应 │ ◀─── │ │
└─────────────┘ └─────────────┘ └─────────────┘
同源 无跨域限制 无需配置 CORS开发环境代理
Webpack Dev Server:
// webpack.config.js
module.exports = {
devServer: {
proxy: {
// 基础配置
'/api': {
target: 'http://api.example.com',
changeOrigin: true
},
// 完整配置
'/api/v2': {
target: 'http://api.example.com',
changeOrigin: true,
secure: false, // 允许 https 目标
pathRewrite: { '^/api/v2': '/v2' },
headers: { 'X-Proxy': 'dev-server' }, // 自定义请求头
onProxyReq: (proxyReq, req, res) => {
console.log('代理请求:', req.url);
}
},
'/service-b': {
target: 'http://service-b.example.com',
pathRewrite: { '^/service-b': '' }
}
}
}
};Vite:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://api.example.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
configure: (proxy, options) => {
// 自定义代理配置
proxy.on('proxyReq', (proxyReq, req) => {
console.log('代理请求:', req.url);
});
}
}
}
}
});生产环境代理
Nginx 反向代理:
# nginx.conf
server {
listen 80;
server_name example.com;
# 前端静态资源
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
# API 代理
location /api {
proxy_pass http://api.example.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /service-b {
proxy_pass http://service-b.example.com;
}
}Node.js 中间件代理
http-proxy-middleware:
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// 基础代理
app.use('/api', createProxyMiddleware({
target: 'http://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}));
// 高级配置
app.use('/api', createProxyMiddleware({
target: 'http://api.example.com',
changeOrigin: true,
ws: true, // 支持 WebSocket 代理
secure: false, // 忽略 https 证书校验
pathRewrite: { '^/api': '' },
onProxyReq: function(proxyReq, req, res) {
console.log('代理请求:', req.method, req.url);
},
onProxyRes: function(proxyRes, req, res) {
console.log('代理响应:', proxyRes.statusCode);
},
onError: function(err, req, res) {
res.status(500).json({ error: 'Proxy error' });
}
}));
app.listen(3000);四、postMessage
window.postMessage 是 HTML5 提供的跨文档通信 API,用于不同窗口之间的安全通信。
跨窗口通信
/**
* 父页面发送消息
*/
// 获取 iframe 引用
const iframe = document.getElementById('myIframe');
// 等待 iframe 加载完成
iframe.onload = function() {
// 发送消息
iframe.contentWindow.postMessage(
{ type: 'USER_DATA', data: { name: 'Alice' } },
'http://child.example.com' // 目标来源,必须指定
);
};
// 父页面监听子页面回复
window.addEventListener('message', function(event) {
// 必须校验来源
if (event.origin !== 'http://child.example.com') return;
console.log('收到子页面消息:', event.data);
});子页面接收和回复:
// child.html 中
window.addEventListener('message', function(event) {
// 校验来源(必须)
if (event.origin !== 'http://parent.example.com') return;
// 回复消息
event.source.postMessage(
{ type: 'REPLY', data: '已收到' },
event.origin
);
});安全验证机制
/**
* 安全的 postMessage 封装
*/
class SecureMessenger {
constructor(targetWindow, allowedOrigins) {
this.targetWindow = targetWindow;
this.allowedOrigins = allowedOrigins;
this.listeners = new Map();
window.addEventListener('message', this.handleMessage.bind(this));
}
// 发送消息
send(type, data) {
this.targetWindow.postMessage({ type, data }, '*');
}
// 监听消息
on(type, callback) {
if (!this.listeners.has(type)) {
this.listeners.set(type, []);
}
this.listeners.get(type).push(callback);
}
// 处理收到的消息
handleMessage(event) {
// 校验来源(安全关键)
if (!this.allowedOrigins.includes(event.origin)) {
console.warn('拒绝来自未知来源的消息:', event.origin);
return;
}
const { type, data } = event.data || {};
const callbacks = this.listeners.get(type);
if (callbacks) {
callbacks.forEach(callback => callback(data, event));
}
}
// 销毁时移除监听器
destroy() {
window.removeEventListener('message', this.handleMessage);
}
}
// 使用示例
const messenger = new SecureMessenger(iframe.contentWindow, ['http://child.example.com']);
messenger.send('GET_DATA', { id: 123 });
// 接收消息
messenger.on('DATA_RESPONSE', (data, event) => {
console.log('收到数据:', data);
});实际应用场景
场景 1: iframe 高度自适应
// 子页面(iframe 内)
function sendHeight() {
const height = document.body.scrollHeight;
parent.postMessage({ type: 'RESIZE', height }, '*');
}
// 监听 DOM 变化
new ResizeObserver(sendHeight).observe(document.body);
window.addEventListener('load', sendHeight);
// 父页面
window.addEventListener('message', function(event) {
if (event.data.type === 'RESIZE') {
document.getElementById('myIframe').style.height = event.data.height + 'px';
}
});场景 2: 跨域登录状态同步
// 主站 a.com
window.addEventListener('message', function(event) {
if (event.origin === 'https://sso.example.com') {
if (event.data.type === 'LOGIN_SUCCESS') {
localStorage.setItem('token', event.data.token);
location.reload();
}
}
});
// SSO 登录页
window.opener.postMessage(
{ type: 'LOGIN_SUCCESS', token: 'xxx' },
'https://a.com'
);
window.close();五、WebSocket
WebSocket 协议不受同源策略限制,可用于跨域双向通信。
跨域特性
┌─────────────┐ ┌─────────────┐
│ 浏览器 │ │ 服务器 │
│ │ │ │
│ WebSocket │ ① WebSocket 握手 │ │
│ 连接请求 │ ─────────────────────────────▶│ │
│ │ Origin: http://example.com │ │
│ │ │ │
│ │ ② 握手响应 │ │
│ │ ◀─────────────────────────────│ │
│ │ │ │
│ │ ③ 双向通信 │ │
│ │ ◀────────────────────────────▶│ │
└─────────────┘ └─────────────┘连接管理
/**
* WebSocket 客户端封装
*/
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectInterval = options.reconnectInterval || 3000;
this.listeners = new Map();
this.connect();
}
// 建立连接
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket 已连接');
this.reconnectAttempts = 0;
this.emit('open');
};
this.ws.onmessage = (event) => {
let data;
try {
data = JSON.parse(event.data);
} catch {
data = event.data;
}
this.emit('message', data);
};
this.ws.onclose = () => {
console.log('连接关闭');
this.emit('close');
// 自动重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = this.reconnectInterval * this.reconnectAttempts;
console.log(`${delay}ms 后重连(第 ${this.reconnectAttempts} 次)`);
setTimeout(() => this.connect(), delay);
}
};
this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
this.emit('error', error);
};
}
// 发送消息
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
// 监听事件
on(type, callback) {
if (!this.listeners.has(type)) {
this.listeners.set(type, []);
}
this.listeners.get(type).push(callback);
}
// 触发事件
emit(type, ...args) {
const callbacks = this.listeners.get(type);
if (callbacks) {
callbacks.forEach(cb => cb(...args));
}
}
// 关闭连接
close() {
if (this.ws) this.ws.close();
}
}
// 使用
const client = new WebSocketClient('ws://api.example.com/socket');
client.send({ type: 'SUBSCRIBE', channel: 'news' });
});
client.on('message', (data) => {
console.log('收到消息:', data);
});实践示例
// 服务器端(Node.js + ws)
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', (ws, req) => {
const origin = req.headers.origin;
// 可选:验证 Origin
const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
if (!allowedOrigins.includes(origin)) {
ws.close();
return;
}
// 接收客户端消息
ws.on('message', (message) => {
const data = JSON.parse(message);
console.log('收到:', data);
// 广播或回显
ws.send(JSON.stringify({ type: 'ECHO', data }));
});
// 连接关闭
ws.on('close', () => {
console.log('客户端断开连接');
});
};
// 客户端(浏览器)
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = function() {
console.log('已连接');
ws.send(JSON.stringify({ type: 'HELLO', msg: '你好' }));
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('收到:', data);
};六、其他方案
以下方案已过时,但在特定场景仍有参考价值。
document.domain
适用于同一主域名下的子域名通信(已废弃):
// a.example.com 页面
document.domain = 'example.com';
// b.example.com 页面
document.domain = 'example.com';
// 现在可以互相访问 DOM
// 注意:此方法已被现代浏览器废弃,不推荐使用window.name
利用 window.name 跨页面传递数据:
/**
* window.name 跨域方案
*/
function getDataFromUrl(url, callback) {
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
let state = 0;
iframe.onload = function() {
if (state === 0) {
state = 1;
// 第一次加载同源空页面
iframe.src = 'about:blank';
} else if (state === 1) {
// 第二次加载完成后,读取 window.name
state = 2;
const data = iframe.contentWindow.name;
callback(JSON.parse(data));
document.body.removeChild(iframe);
}
};
iframe.src = url;
document.body.appendChild(iframe);
}
// 使用
getDataFromUrl('http://other-domain.com/data.html', (data) => {
console.log('获取到数据:', data);
});
// data.html 内容
// <script>
// window.name = '{"name":"Alice","age":25}';
// </script>方案对比与选择
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| CORS | API 请求、前后端分离项目 | 标准化、支持所有 HTTP 方法、浏览器原生支持 | 需要服务器配置 |
| JSONP | 兼容老系统、简单 GET 请求 | 兼容性好、实现简单 | 只支持 GET、有安全风险 |
| 代理服务器 | 开发环境、无法修改服务端的项目 | 前端无需修改、对后端透明 | 需要额外部署代理服务 |
| postMessage | iframe 嵌套、跨窗口通信 | 安全可控、双向通信 | 需要双方配合 |
| WebSocket | 实时通信、推送服务 | 双向通信、低延迟 | 需要 WebSocket 服务端支持 |
选择建议:
┌─────────────────────────────────────────────────────────────┐
│ 方案选择决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 需要跨域请求 API? │
│ │ │
│ ├─── 是 ──▶ 能否修改服务器? │
│ │ │ │
│ │ ├─── 是 ──▶ 使用 CORS │
│ │ │ │
│ │ └─── 否 ──▶ 使用代理服务器 │
│ │ │
│ └─── 否 ──▶ iframe 通信? │
│ │ │
│ ├─── 是 ──▶ 使用 postMessage │
│ │ │
│ └─── 否 ──▶ 实时通信? │
│ │ │
│ ├─── 是 ──▶ WebSocket │
│ │ │
│ └─── 否 ──▶ JSONP │
│ │
└─────────────────────────────────────────────────────────────┘安全最佳实践
// 1. CORS 配置不要过于宽松
// ❌ 危险配置
app.use(cors({ origin: '*' })); // 允许任何来源
app.use(cors({ origin: '*', credentials: true })); // 无效配置
// ✅ 安全配置
const allowedOrigins = ['https://example.com', 'https://app.example.com'];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true // 允许携带 Cookie
}));
// 2. 服务端验证 WebSocket Origin(防止 CSRF 跨站攻击)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080, verifyClient: (info) => {
const allowedOrigins = ['http://example.com', 'http://localhost:3000'];
if (!allowedOrigins.includes(info.origin)) {
return false; // 拒绝连接
}
return true;
}});
wss.on('connection', (ws, req) => {
if (!allowedOrigins.includes(req.headers.origin)) {
ws.close(1008, 'Origin not allowed');
return;
}
// 正常处理
});常见问题解答
Q1: 为什么配置了 CORS 还是报跨域错误?
A: 常见原因:
-
服务器未正确设置响应头
javascript// 检查是否在所有需要的路由上都设置了 CORS app.use(cors()); // 确保在路由之前 -
预检请求未处理
javascript// 确保处理 OPTIONS 请求 app.options('*', cors()); -
携带凭证时使用了
*javascript// ❌ 错误 res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Credentials', 'true'); // ✅ 正确 res.header('Access-Control-Allow-Origin', 'http://example.com'); res.header('Access-Control-Allow-Credentials', 'true');
Q2: 本地开发时如何处理跨域?
A: 推荐使用开发服务器代理:
// Vite
export default {
server: {
proxy: {
'/api': 'http://api.example.com'
}
}
}
// Webpack
module.exports = {
devServer: {
proxy: {
'/api': 'http://api.example.com'
}
}
}Q3: 如何调试跨域问题?
A: 使用浏览器开发者工具:
- 打开 DevTools → Network
- 查看请求 Headers 中的
Origin - 查看响应 Headers 中的 CORS 相关字段
- Console 中查看具体错误信息
// 在服务器端添加调试日志
app.use((req, res, next) => {
console.log('Request Origin:', req.headers.origin);
console.log('Request Method:', req.method);
next();
});Q4: CORS 会影响性能吗?
A: 对于非简单请求,会产生额外的预检请求开销。优化方法:
- 使用简单请求:尽量使用 GET、POST,避免复杂请求头
- 缓存预检结果:设置
Access-Control-Max-Age - 合并请求:减少跨域请求次数
// 设置预检缓存
res.header('Access-Control-Max-Age', '86400'); // 24 小时Q5: 如何实现多域名 CORS?
A: 动态设置 Access-Control-Allow-Origin:
app.use((req, res, next) => {
const allowedOrigins = [
'https://example.com',
'https://app.example.com',
'http://localhost:3000'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
next();
});💡 提示:CORS 是现代 Web 跨域解决方案的首选,掌握其原理和配置对前后端开发都至关重要。在生产环境中,务必注意安全配置,避免使用过于宽松的跨域策略。