前端跨域请求原理及实践
在前端开发中,"跨域"是一个绕不开的话题。当我们的页面尝试从一个域名请求另一个域名的资源时,浏览器往往会抛出类似Access to fetch at 'xxx' from origin 'xxx' has been blocked by CORS policy
的错误。下面将深入探讨跨域请求的底层原理,并介绍多种解决跨域问题和解决方案。
一、跨域的本质:同源策略
要理解跨域,首先需要了解浏览器的同源策略(Same-Origin Policy)。这是浏览器最核心的安全功能之一,由Netscape在1995年引入,其目的是防止恶意网页窃取另一个网页的敏感数据。
1.1 什么是"同源"?
两个URL被视为"同源",必须同时满足以下三个条件:
- 协议相同(如都是http或https)
- 域名相同(如都是example.com,而非a.example.com和b.example.com)
- 端口相同(如都是80端口,默认端口可省略)
举例说明:
当前页面URL | 请求目标URL | 是否同源 | 原因 |
---|---|---|---|
http://example.com | http://example.com/page | 是 | 三要素完全相同 |
http://example.com | https://example.com | 否 | 协议不同(http vs https) |
http://example.com | http://api.example.com | 否 | 域名不同(主域 vs 子域) |
http://example.com:80 | http://example.com:8080 | 否 | 端口不同(80 vs 8080) |
1.2 同源策略的限制范围
同源策略主要限制以下几种交互:
- DOM访问:禁止不同源页面之间的DOM操作(如iframe嵌套的跨域页面)
- 数据读取:禁止读取不同源的Cookie、LocalStorage、SessionStorage
- 网络请求:禁止通过XMLHttpRequest、Fetch API等方式发起跨域HTTP请求
注意:并非所有跨域请求都会被禁止。像<img>
、<script>
、<link>
等标签的资源加载不受同源策略限制,这也是后续某些跨域解决方案的技术基础。
二、跨域请求的类型:简单请求与预检请求
当浏览器检测到跨域请求时,会根据请求的特征将其分为两类,并采取不同的处理策略:
2.1 简单请求(Simple Request)
同时满足以下条件的请求被视为简单请求:
- 请求方法为以下三种之一:
GET
、HEAD
、POST
- 请求头仅包含浏览器默认字段或以下字段:
Accept
、Accept-Language
、Content-Language
、Content-Type
(仅限特定值) Content-Type
的值只能是:application/x-www-form-urlencoded
、multipart/form-data
、text/plain
简单请求的处理流程:
- 浏览器直接发送请求,并在请求头中添加
Origin
字段(值为当前页面域名) - 服务器响应时,若包含
Access-Control-Allow-Origin
且值包含请求的Origin
,则浏览器允许前端读取响应;否则拦截响应,抛出跨域错误
2.2 预检请求(Preflight Request)
不满足简单请求条件的跨域请求会触发预检请求,例如:
- 使用
PUT
、DELETE
等特殊请求方法 - 请求头包含自定义字段(如
Authorization
、X-Requested-With
) Content-Type
为application/json
预检请求的处理流程:
- 浏览器先发送一个
OPTIONS
方法的预检请求,询问服务器是否允许实际请求 - 服务器响应预检请求时,通过
Access-Control-*
系列头字段声明允许的跨域规则 - 若服务器允许,浏览器才发送实际请求;否则直接拦截,不发送实际请求
三、跨域解决方案及实践
了解跨域的原理后,我们来介绍几种常用的跨域解决方案,每种方案都将提供完整的代码示例。
3.1 CORS(Cross-Origin Resource Sharing)
CORS是W3C标准推荐的跨域解决方案,通过服务器端设置响应头实现跨域允许,支持所有HTTP方法,是目前最主流的跨域方案。
3.1.1 基本原理
CORS的核心是服务器端通过设置Access-Control-*
系列响应头,告知浏览器允许哪些跨域请求。常用头字段包括:
Access-Control-Allow-Origin
:允许的源(如https://example.com
或*
表示允许所有)Access-Control-Allow-Methods
:允许的请求方法(如GET, POST, PUT
)Access-Control-Allow-Headers
:允许的请求头Access-Control-Allow-Credentials
:是否允许携带凭证(Cookie等)Access-Control-Max-Age
:预检请求的缓存时间(避免重复发送预检请求)
3.1.2 代码示例
前端代码(使用Fetch API):
// 前端页面地址:http://localhost:3000
fetch('http://localhost:4000/api/data', {method: 'POST',headers: {'Content-Type': 'application/json','X-Custom-Header': 'custom-value' // 自定义头,会触发预检请求},body: JSON.stringify({ name: '前端请求' }),credentials: 'include' // 允许携带Cookie
})
.then(response => response.json())
.then(data => console.log('跨域请求成功:', data))
.catch(error => console.error('跨域请求失败:', error));
后端代码(Node.js + Express):
// 服务器地址:http://localhost:4000
const express = require('express');
const app = express();
app.use(express.json());// CORS配置中间件
app.use((req, res, next) => {// 允许的源(生产环境建议指定具体域名,而非*)res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');// 允许的请求方法res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');// 允许的请求头(需包含前端实际使用的所有自定义头)res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header');// 允许携带凭证(Cookie等)res.setHeader('Access-Control-Allow-Credentials', 'true');// 预检请求缓存时间(秒)res.setHeader('Access-Control-Max-Age', '86400'); // 24小时// 处理预检请求(直接返回204)if (req.method === 'OPTIONS') {return res.sendStatus(204);}next();
});// 接口路由
app.post('/api/data', (req, res) => {console.log('收到跨域请求数据:', req.body);res.json({ status: 'success', message: '跨域请求处理完成',data: req.body});
});app.listen(4000, () => {console.log('CORS服务器运行在 http://localhost:4000');
});
注意:当Access-Control-Allow-Credentials
设为true
时,Access-Control-Allow-Origin
不能设为*
,必须指定具体域名。
3.2 JSONP(JSON with Padding)
JSONP是一种古老但兼容性极佳的跨域方案(支持IE等老浏览器),其原理是利用<script>
标签不受同源策略限制的特性,通过动态创建<script>
标签发起跨域请求。
3.2.1 基本原理
- 前端定义一个回调函数(如
handleJsonpResponse
) - 前端动态创建
<script>
标签,其src
指向跨域接口,并在URL中携带回调函数名(如?callback=handleJsonpResponse
) - 服务器接收到请求后,将数据包裹在回调函数中返回(如
handleJsonpResponse({...})
) - 浏览器加载
<script>
后,自动执行回调函数,前端即可获取数据
3.2.2 代码示例
前端代码:
<!-- 前端页面地址:http://localhost:3000 -->
<script>
// 定义回调函数
function handleJsonpResponse(data) {console.log('JSONP跨域请求成功:', data);
}// 动态创建script标签发起请求
function requestJsonp() {const script = document.createElement('script');// 跨域接口地址,携带回调函数名script.src = 'http://localhost:4000/api/jsonp?callback=handleJsonpResponse&name=jsonp请求';document.body.appendChild(script);// 请求完成后移除script标签script.onload = () => script.remove();script.onerror = () => {console.error('JSONP请求失败');script.remove();};
}
</script><button onclick="requestJsonp()">发起JSONP请求</button>
后端代码(Node.js + Express):
// 服务器地址:http://localhost:4000
const express = require('express');
const app = express();app.get('/api/jsonp', (req, res) => {const { callback, name } = req.query;console.log('收到JSONP请求参数:', name);// 构造响应数据(用回调函数包裹)const data = {status: 'success',message: 'JSONP请求处理完成',data: { name }};// 返回JavaScript代码(执行回调函数)res.send(`${callback}(${JSON.stringify(data)})`);
});app.listen(4000, () => {console.log('JSONP服务器运行在 http://localhost:4000');
});
局限性:
- 仅支持
GET
请求 - 安全性风险(可能遭受XSS攻击)
- 无法捕获HTTP错误状态码(如404、500)
3.3 代理服务器
代理服务器是开发环境中常用的跨域解决方案,其原理是:由于浏览器的同源策略只限制前端脚本,不限制服务器之间的通信,因此可以通过一个与前端同源的代理服务器转发请求到目标服务器。
3.3.1 开发环境代理(以Vite为例)
在前端项目中(如Vue、React),可通过开发服务器配置代理,解决开发阶段的跨域问题。
Vite配置示例(vite.config.js):
// 前端开发服务器:http://localhost:5173
export default {server: {// 配置代理proxy: {// 匹配所有以/api开头的请求'/api': {target: 'http://localhost:4000', // 目标服务器地址changeOrigin: true, // 发送请求时,将Host头改为目标服务器地址// 可选:重写路径(如果目标接口没有/api前缀)// rewrite: (path) => path.replace(/^\/api/, '')}}}
};
前端请求代码:
// 此时请求的是同源的开发服务器(http://localhost:5173),无跨域问题
// 开发服务器会自动转发到 http://localhost:4000/api/data
fetch('/api/data', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ name: '通过代理请求' })
})
.then(response => response.json())
.then(data => console.log('代理请求成功:', data));
3.3.2 生产环境代理(Nginx)
生产环境中,可通过Nginx反向代理实现跨域,配置示例如下:
# Nginx配置
server {listen 80;server_name localhost;# 前端页面所在目录location / {root /path/to/frontend;index index.html;}# 代理跨域请求location /api/ {# 目标服务器地址proxy_pass http://localhost:4000/api/;# 传递原始请求头proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;# 可选:设置CORS头(如果目标服务器未设置)add_header Access-Control-Allow-Origin *;}
}
配置后,前端直接请求/api/data
,Nginx会自动转发到http://localhost:4000/api/data
,避免跨域问题。
3.4 其他跨域方案
3.4.1 iframe + postMessage
适用于两个跨域页面之间的通信(如父页面与iframe嵌套页面):
父页面(http://parent.com):
<iframe id="childFrame" src="http://child.com"></iframe><script>
// 向子页面发送消息
const frame = document.getElementById('childFrame');
frame.onload = () => {frame.contentWindow.postMessage({ type: 'greeting', data: 'Hello from parent' },'http://child.com' // 限制接收域);
};// 接收子页面消息
window.addEventListener('message', (event) => {// 验证消息来源if (event.origin !== 'http://child.com') return;console.log('收到子页面消息:', event.data);
});
</script>
子页面(http://child.com):
// 接收父页面消息
window.addEventListener('message', (event) => {if (event.origin !== 'http://parent.com') return;console.log('收到父页面消息:', event.data);// 向父页面回复消息event.source.postMessage({ type: 'response', data: 'Hello from child' },event.origin);
});
3.4.2 WebSocket
WebSocket协议本身不受同源策略限制,可直接建立跨域连接:
前端代码:
// 建立WebSocket连接(ws/wss协议)
const socket = new WebSocket('ws://localhost:4000');// 连接成功
socket.onopen = () => {console.log('WebSocket连接已建立');socket.send('Hello WebSocket');
};// 接收消息
socket.onmessage = (event) => {console.log('收到WebSocket消息:', event.data);
};
后端代码(Node.js + ws库):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 4000 });wss.on('connection', (ws) => {console.log('客户端已连接');ws.on('message', (message) => {console.log('收到消息:', message.toString());ws.send('服务器收到:' + message.toString());});
});
四、总结与最佳实践
跨域请求的解决方案各有优缺点,选择时需根据实际场景判断:
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
CORS | 功能完善、支持所有HTTP方法、安全性高 | 需要服务器配合、老浏览器兼容问题 | 现代Web应用(推荐) |
JSONP | 兼容性好(支持IE) | 仅支持GET、安全性差 | 需兼容老浏览器的场景 |
代理服务器 | 前端无需修改、开发/生产均可用 | 需要额外配置服务器 | 开发环境调试、生产环境跨域 |
iframe + postMessage | 适合页面间通信 | 仅用于页面交互、不适合API请求 | 父页面与iframe跨域通信 |
WebSocket | 全双工通信、无跨域限制 | 需专门协议、不适合普通API请求 | 实时通信场景(如聊天、通知) |
最佳实践建议:
- 优先使用CORS,这是最标准、最安全的跨域方案
- 开发环境使用代理服务器(如Vite、Webpack代理)提高开发效率
- 生产环境避免使用
*
作为Access-Control-Allow-Origin
,严格限制允许的源 - 涉及用户凭证的请求,确保正确配置
Access-Control-Allow-Credentials
- 避免使用JSONP,除非有强烈的老浏览器兼容需求
通过本文的介绍,相信你已经对跨域请求的原理和解决方案有了全面的理解。在实际开发中,结合具体场景选择合适的方案,就能轻松解决跨域问题。