为什么会有跨域这个问题产生?
跨域问题的根源在于浏览器的同源策略(Same-Origin Policy) ,这是现代浏览器为了保障用户信息安全而实施的一种核心安全机制。
浏览器为什么需要“同源策略”这种安全机制?
一、防御恶意攻击
-
防止数据窃取
- 场景:用户登录银行网站(
bank.com)后,访问了一个恶意网站(evil.com)。如果没有同源策略,恶意网站的脚本可以直接通过 AJAX 请求bank.com的 API,窃取用户账户数据。 - 同源策略的作用:禁止跨域读取其他源的资源(如 AJAX 响应、DOM 内容),阻止此类攻击。
- 场景:用户登录银行网站(
-
阻止跨站脚本攻击(XSS)的扩大化
- 即使网站存在 XSS 漏洞(攻击者注入了恶意脚本),同源策略也能限制该脚本仅能访问当前源的数据,而无法直接读取用户在其他网站(如邮箱、社交网络)的敏感信息。
二、保护用户身份和隐私
-
隔离 Cookie 和本地存储
- 问题:Cookie 通常用于身份验证。若允许跨域访问,恶意网站可读取其他源的 Cookie,直接冒充用户身份。
- 同源策略的解决方案:只有同源的页面才能读写当前源的 Cookie、LocalStorage 等数据。
-
防止跨站请求伪造(CSRF)
- 攻击原理:恶意网站诱导用户点击链接,向用户已登录的网站(如
bank.com)发起请求(如转账操作),利用浏览器自动携带 Cookie 的特性完成攻击。 - 同源策略的限制:虽然不能完全阻止 CSRF,但它限制了恶意脚本通过 AJAX 直接读取响应结果,增加了攻击难度(需配合其他机制如 CSRF Token)。
- 攻击原理:恶意网站诱导用户点击链接,向用户已登录的网站(如
三、控制资源访问权限
-
限制 DOM 的跨源访问
- 场景:一个页面通过
<iframe>嵌入其他网站的内容(如广告)。若允许跨域访问 DOM,父页面可能窃取用户输入的密码或篡改内容。 - 同源策略的规则:禁止父页面通过 JavaScript 读取或修改不同源 iframe 的 DOM。
- 场景:一个页面通过
-
隔离浏览器存储和缓存
- 不同源的页面无法直接访问彼此的 IndexedDB、SessionStorage 等存储,防止数据泄露。
什么是同源策略
同源策略,是浏览器对 JavaScript 实施的安全限制,只要
协议、域名、端口有任何一个不同,都被当作是不同的域。被浏览器禁止访问。
跨域问题的解决方案
一、JSONP:历史遗留的妥协方案
ajax 请求受同源策略影响,不允许进行跨域请求,而 script 标签 src 属性中的链 接却可以访问跨域的 js 脚本,利用这个特性,服务端不再返回 JSON 格式的数据,而是 返回一段调用某个函数的 js 代码,在 src 中进行了调用,这样实现了跨域。
步骤:
- 创建唯一回调函数名(防止污染全局)
- 动态创建
<script>标签 - 服务器响应数据
- 插入
<script>标签触发请求 - 自动执行回调函数
完整优化代码示例
/**
* 实现 JSONP 跨域请求的通用函数
* @param {string} url 请求的接口地址(需支持 JSONP)
* @param {object} params 请求参数(将转换为 URL 查询参数)
* @param {number} [timeout=5000] 超时时间(毫秒)
* @returns {Promise} 返回 Promise 对象,成功时 resolve 数据,失败时 reject 错误
*/
function jsonp(url, params = {}, timeout = 5000) {
return new Promise((resolve, reject) => {
// ================ 1. 生成唯一回调函数名 ================
// 避免全局命名冲突,格式示例:jsonp_callback_1651234567890_4a3b2c
const callbackName = `jsonp_callback_${Date.now()}_${Math.random().toString(16).slice(2)}`;
// ================ 2. 构建请求 URL ================
// 使用 URLSearchParams 自动编码参数,防止 XSS 和格式错误
const queryParams = new URLSearchParams({
...params, // 合并用户传入的参数
callback: callbackName // 添加 JSONP 回调参数
}).toString();
// 拼接完整 URL(示例:http://api.com/data?id=123&callback=jsonp_callback_...)
const requestUrl = `${url}?${queryParams}`;
// ================ 3. 创建 script 标签 ================
const script = document.createElement('script');
script.src = requestUrl;
// ================ 4. 超时处理 ================
let timeoutId = setTimeout(() => {
reject(new Error(`JSONP request to ${url} timed out after ${timeout}ms`));
cleanup(); // 触发清理
}, timeout);
// ================ 5. 错误处理 ================
script.onerror = (err) => {
reject(new Error(`JSONP request to ${url} failed`));
cleanup();
};
// ================ 6. 注册全局回调函数 ================
// 将回调绑定到 window 对象,服务器返回的脚本将调用此函数
window[callbackName] = (responseData) => {
resolve(responseData); // 成功时传递数据
cleanup(); // 无论成功失败都要清理
};
// ================ 7. 发起请求 ================
document.body.appendChild(script); // 插入 DOM 后浏览器自动加载脚本
// ================ 8. 清理函数 ================
// 移除 script 标签、清除超时、删除全局回调
function cleanup() {
if (script.parentNode) {
document.body.removeChild(script);
}
clearTimeout(timeoutId);
delete window[callbackName]; // 避免内存泄漏
}
});
}
// ==================== 使用示例 ====================
// 调用公开的 JSONP 测试接口(实际项目需替换为真实接口)
jsonp('https://api.example.com/data', { id: 123 })
.then(data => {
console.log('请求成功,数据:', data);
})
.catch(err => {
console.error('请求失败:', err.message);
});
JSONP 的缺点:
- 因为 script 标签只能使用 get 请求,法处理复杂场景(如文件上传)
- JSONP 需要后端配合返回指定格式的数据,易导致全局污染。
- 存在 XSS 风险(若服务器返回恶意代码)
二、CORS:现代跨域的标准化方案
CORS(Cross-origin resource sharing)跨域资源共享 是一种机制,是目前主流的跨域解决方案,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。
服务器设置对 CORS 的支持原理:服务器设置 Access-Control-Allow-Origin HTTP 响应头之后,浏览器将会允许跨域请求
一、CORS 的核心流程
1. 浏览器自动添加 Origin 请求头
- 触发条件:当发起 跨域请求 时,浏览器自动在请求头中添加
Origin字段,值为当前页面源(如https://a.com)。 - 例外场景:同源请求、某些浏览器扩展请求可能不添加。
- 示例请求头:
GET /api/data HTTP/1.1 Origin: https://a.com Host: b.com
2. 服务器响应 CORS 头
- 关键响应头:
响应头 作用 Access-Control-Allow-Origin允许的源(如 https://a.com或*,*不允许携带凭证)Access-Control-Allow-Methods允许的 HTTP 方法(如 GET, POST, PUT)Access-Control-Allow-Headers允许的自定义请求头(如 X-ABC, Content-Type)Access-Control-Max-Age预检请求缓存时间(秒),减少重复预检(如 86400)Access-Control-Allow-Credentials是否允许携带凭证(如 Cookie,需配合前端 withCredentials: true)
二、简单请求 vs. 预检请求
1. 简单请求条件(不触发预检)
- HTTP 方法:
GET、POST、HEAD - 请求头:仅限以下安全集合:
Accept, Accept-Language, Content-Language, Content-Type - Content-Type:仅限
text/plain、multipart/form-data、application/x-www-form-urlencoded
示例:
// 简单请求:GET + 无自定义头
fetch('https://b.com/api');
// 简单请求:POST + application/x-www-form-urlencoded
fetch('https://b.com/api', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'key=value'
});
2. 预检请求(OPTIONS)触发条件
- HTTP 方法:
PUT、DELETE、PATCH等非简单方法 - 自定义请求头:如
X-ABC、Authorization - Content-Type:
application/json、text/xml等非简单值
示例:
// 触发预检:PUT + 自定义头 + JSON
fetch('https://b.com/api', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-ABC': 'custom'
},
body: JSON.stringify({ data: 123 })
});
三、预检请求完整流程
1. 预检请求(OPTIONS)
- 请求头:
OPTIONS /api HTTP/1.1 Origin: https://a.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-ABC, Content-Type
2. 服务器响应预检
- 合法响应:
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://a.com Access-Control-Allow-Methods: PUT, POST, GET, OPTIONS Access-Control-Allow-Headers: X-ABC, Content-Type Access-Control-Max-Age: 86400
3. 实际请求
- 浏览器缓存预检结果(根据
Max-Age),后续相同请求在有效期内跳过预检。 - 实际请求头:
PUT /api HTTP/1.1 Origin: https://a.com X-ABC: custom Content-Type: application/json
在这里有些面试官会顺藤摸瓜问你,如何给 CORS 设置 Cookie 或 认证头(如 Authorization) ?
这里顺便讲一下。在 CORS(跨域资源共享)请求中,若需传递 Cookies 或 认证头(如 Authorization),需同时满足 前端设置 和 后端配置 的条件。
必要条件
| 角色 | 条件 |
|---|---|
| 前端 | 1. 设置 credentials: 'include' 或 withCredentials: true2. 避免使用通配符 * 的请求头 |
| 后端 | 1. 返回 Access-Control-Allow-Credentials: true2. 精确指定 Access-Control-Allow-Origin(非 *)3. 处理 OPTIONS 预检请求并返回正确的头 4. 允许相关请求头(如 Authorization) |
| 浏览器 | 校验上述条件,若任一不满足,请求将被拦截。 |
一、前端设置
- 启用
withCredentials模式
- 在发起请求时,需显式设置请求的凭证携带模式:
- Fetch API:
fetch('https://api.example.com/data', { credentials: 'include' // 携带 Cookies 和认证头 }); - XMLHttpRequest:
const xhr = new XMLHttpRequest(); xhr.withCredentials = true; - Axios:
axios.get('https://api.example.com/data', { withCredentials: true });
- Fetch API:
- 避免使用通配符
*的请求头- 若请求中包含自定义头(如
Authorization),需在headers中明确指定:fetch('https://api.example.com/data', { headers: { 'Authorization': 'Bearer token' }, credentials: 'include' });
- 若请求中包含自定义头(如
二、后端配置
- 设置
Access-Control-Allow-Credentials头- 必须返回
Access-Control-Allow-Credentials: true,否则浏览器会拒绝请求。HTTP/1.1 200 OK Access-Control-Allow-Credentials: true
- 必须返回
- 精确指定
Access-Control-Allow-Origin- 禁止使用通配符
*,必须明确指定允许的来源(与请求头Origin一致):Access-Control-Allow-Origin: https://your-frontend-domain.com
- 禁止使用通配符
- 处理预检请求(OPTIONS)
- 若请求为复杂请求(如包含自定义头或非简单方法),需正确处理
OPTIONS预检请求:// Node.js Express 中间件示例 app.options('/api', (req, res) => { res.setHeader('Access-Control-Allow-Origin', 'https://your-frontend-domain.com'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT'); res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.status(200).send(); });
- 若请求为复杂请求(如包含自定义头或非简单方法),需正确处理
- 允许必要的请求头
- 若请求包含自定义头(如
Authorization),需在Access-Control-Allow-Headers中列出:Access-Control-Allow-Headers: Authorization, Content-Type
- 若请求包含自定义头(如
当下最常用的方式,正向代理和反向代理
正向代理(开发环境)
正向代理是 客户端侧的代理服务器,代表客户端向外部服务器发起请求。客户端通过正向代理访问互联网资源,隐藏了客户端的真实身份。
工作原理
客户端 → 正向代理 → 互联网资源
Vite 代理配置
在 vite.config.js 中,server.proxy 的配置需注意以下细节:
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://backend-server.com',
changeOrigin: true, // 关键:修改 Origin 为目标地址,避免后端拦截
rewrite: (path) => path.replace(/^\/api/, '/v1'), // 路径替换(支持复杂逻辑)
configure: (proxy, options) => {
// 高级:自定义代理行为(如修改请求头)
proxy.on('proxyReq', (proxyReq, req, res) => {
proxyReq.setHeader('X-Forwarded-For', req.ip);
});
},
ws: true, // 代理 WebSocket 请求
},
'/socket.io': {
target: 'ws://backend-server.com',
ws: true, // 显式代理 WebSocket
}
}
}
});
关键配置项补充
| 配置项 | 说明 |
|---|---|
configure | 允许通过 http-proxy 的 API 自定义代理逻辑(如添加请求头、日志记录) |
ws | 代理 WebSocket 请求(默认启用,但显式声明更清晰) |
rewrite | 支持函数或正则表达式,实现动态路径映射(如版本号替换) |
Webpack 代理配置的进阶用法
在 vue.config.js 或 webpack.config.js 中,devServer.proxy 的扩展配置:
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://backend-server.com',
changeOrigin: true,
pathRewrite: {
'^/api/old': '/api/new', // 多路径重写规则
'^/api': ''
},
onProxyReq: (proxyReq, req, res) => {
// 请求发出前修改逻辑(如添加认证头)
proxyReq.setHeader('Authorization', 'Bearer token');
},
onProxyRes: (proxyRes, req, res) => {
// 修改响应(如剥离冗余头)
delete proxyRes.headers['x-powered-by'];
}
}
}
}
};
高级功能说明
| 功能 | 应用场景 |
|---|---|
onProxyReq | 动态注入请求头、记录请求日志、修改请求体 |
onProxyRes | 清理敏感响应头、统一错误格式、添加缓存控制头 |
pathRewrite | 同时处理多个路径替换规则(按对象键的顺序匹配) |
反向代理(生产环境)
反向代理是 服务器侧的代理服务器,代表服务端接收客户端请求,并将请求转发给内部服务器。客户端不感知后端真实服务器,隐藏了服务端架构细节。
工作原理
客户端 → 反向代理 → 内部服务器(如 Server 1, Server 2)
反向代理需要后端配合使用Nginx 来实现
正向代理 vs 反向代理的适用场景
| 类型 | 优点 | 缺点 | 适用阶段 |
|---|---|---|---|
| 正向代理 | 配置简单,适合快速开发调试 | 仅限开发环境,性能有限 | 本地开发 |
| 反向代理 | 高性能,隐藏后端,生产级安全 | 需运维知识,配置复杂 | 生产环境 |
window.postMessage() — 安全标准的跨域通信方式
原理: HTML5 提供的标准 API,可以让来自不同源(协议 + 域名 + 端口)的窗口之间安全通信。主要用于解决不同窗口(或 iframe)间的数据传递问题,
使用方法:
// A 页面发送消息
otherWindow.postMessage('hello', 'https://example.com');
// B 页面接收消息
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted.com') {
console.log('收到消息:', event.data);
}
});
特点:
- 支持任意跨域。
- 安全:必须验证
event.origin。
WebSocket — 跨域天生支持的通信协议
原理: WebSocket 协议是独立于 HTTP 的,它建立连接后是全双工通信,不受浏览器的同源策略限制。
使用方法:
const socket = new WebSocket('wss://api.example.com/socket');
socket.onmessage = function(event) {
console.log('服务器消息:', event.data);
};
特点:
- 天生支持跨域。
- 用于实时通信,如聊天、股票行情、车联网。
- 服务端需支持 WebSocket 协议。
document.domain — 同基础域名间通信(已被逐步弃用)
原理: 如果两个页面的主域名相同,子域不同(如
a.example.com和b.example.com),可通过设置document.domain = 'example.com'实现通信。
使用方法(双方都需设置):
document.domain = 'example.com'; // 双方必须一致
// 然后可直接访问 iframe 中的 DOM
限制:
- 只能在相同主域的子域间使用。
- 不支持现代浏览器的 strict mode 和某些 CSP 策略。
- 不推荐,已过时。
window.name — 一种老旧但巧妙的跨域通信方法
原理:在浏览器中,
window.name属性在跨域跳转后仍会保留。可借助 iframe 中跳转实现跨域数据传输。
使用方法(简化):
<iframe src="http://a.com/prepare-data.html" name="dataFrame"></iframe>
<!-- prepare-data.html 脚本中设置:window.name = 'some data'; -->
<!-- 跳转后再在父页面读取 window.frames['dataFrame'].name -->
特点:
- 可绕过同源策略。
- 不依赖任何协议。
- 缺点:实现复杂、易被误用、不适合实时交互。
总结
| 方法 | 原理简述 | 优点 | 缺点 | 适用场景 | 是否推荐 |
|---|---|---|---|---|---|
| CORS | 服务器在响应头中添加跨域许可 | 简单标准、安全、现代浏览器支持广泛 | 需服务器配合配置,不支持低版本 IE | 前后端分离接口调用,REST API 通信 | ✅ 推荐 |
| JSONP | 利用 <script> 标签不受同源限制 | 兼容老浏览器、实现简单 | 仅支持 GET 请求、不安全、容易被 XSS 利用 | 老系统兼容、无需服务器修改的 GET 请求 | ❌ 不推荐 |
| window.postMessage | HTML5 提供的跨源安全通信 API | 安全、标准、支持任意跨域、结构清晰 | 需手动验证来源、防止误收消息 | iframe、子窗口、WebView 跨域通信 | ✅ 推荐 |
| WebSocket | 使用 ws:// 或 wss:// 建立持久连接 | 天生支持跨域、全双工通信、实时性强 | 服务端需支持 WebSocket 协议 | 实时应用(聊天、游戏、物联网等) | ✅ 推荐 |
| Nginx 反向代理 | 服务端配置代理避免跨域(前端看不到跨域) | 配置灵活、安全性高、无浏览器限制 | 配置复杂度较高、需要部署控制权 | 生产环境统一代理跨域请求 | ✅ 推荐 |
| document.domain | 设置相同主域的子域名页面共享 JS 权限 | 实现简单、曾广泛用于子域间通信 | 仅限同主域、已被多数浏览器标记为过时 | 同主域(如 a.example.com 与 b.example.com) | ❌ 不推荐 |
| window.name | 利用 window.name 在跨域跳转后仍能保留数据 | 可用于任意跨域、无需服务器支持 | 实现复杂、效率低、易出 bug | 页面跳转后带数据传递(例如下载文件前认证) | ⚠️ 仅限特殊 |
| location.hash + iframe | 通过修改 hash 在 iframe 中传递数据 | 兼容旧浏览器、无需服务器支持 | 实现复杂、仅支持短字符串、通信延迟高 | 老系统 iframe 通信 | ❌ 不推荐 |
| WebRTC 数据通道 | P2P 通信机制,用于浏览器间直接通信 | 实时性强、跨域传输大文件或媒体流 | 实现复杂、需 NAT 打洞和信令服务器支持 | 音视频通话、P2P 数据通信 | ⚠️ 需评估 |