前端跨域的5中解决办法(反向代理、websocket、cors、jsonp、document.domain)
前端跨域的5中解决办法(反向代理、websocket、cors、jsonp、document.domain)
文章目录
1. CORS (跨域资源共享) - 最主流、最推荐的方法
CORS全称是跨域资源共享(Cross-orgin resource sharing)
这是由 W3C 标准提供的官方方案,需要后端配合实现。后端服务器通过在 HTTP 响应头中设置特定的字段,来告诉浏览器允许某个源、方法或头进行跨域访问。
原理:
浏览器将CORS请求分为两类:简单请求和非简单请求。对于非简单请求(如Content-Type为application/json
或使用了自定义头),浏览器会先发送一个OPTIONS
方法的“预检请求”(Preflight Request),预检通过后才会发送真正的请求。
后端设置示例 (Node.js/Express):
const express = require('express');
const app = express();
// 中间件:设置CORS响应头
app.use((req, res, next) => {
// 允许请求的来源(* 表示允许任何来源,但出于安全考虑,生产环境应指定具体域名)
res.header('Access-Control-Allow-Origin', 'http://localhost:8080');
// 允许客户端携带的请求头(如 Authorization, Content-Type)
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
// 允许的HTTP方法
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// 允许浏览器在跨域请求中携带Cookie(Access-Control-Allow-Origin 不能为 *,需指定具体域名)
res.header('Access-Control-Allow-Credentials', 'true');
// 如果是OPTIONS请求(预检请求),直接返回200
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
// 你的API路由
app.get('/api/data', (req, res) => {
res.json({ message: 'This is data from CORS-enabled API!' });
});
app.listen(3000);
前端无需特殊处理,正常使用fetch
、axios
等发送请求即可。但如果设置了Credentials
,前端也需要对应配置:
// 使用 axios
axios.get('http://api.other-site.com/data', {
withCredentials: true // 告诉浏览器在跨域请求时携带cookie等凭证信息
});
// 使用 fetch
fetch('http://api.other-site.com/data', {
credentials: 'include' // 等同于 withCredentials: true
});
CORS 简单请求 vs. 非简单请求
特性 | 简单请求 (Simple Request) | 非简单请求 (Not-Simple Request) |
---|---|---|
触发条件 | 必须同时满足所有以下条件: 1. 方法为 GET, POST, 或 HEAD 2. Content-Type 仅为: text/plain , multipart/form-data , application/x-www-form-urlencoded 3. 未使用自定义头部 |
不满足上述任一条件的请求即为非简单请求 常见情况: - 方法为 PUT, DELETE, PATCH 等 - Content-Type 为 application/json - 使用了自定义头部 (如 Authorization , X-Custom-Header ) |
核心机制 | 直接请求 | 预检请求 (Preflight Request) |
通信过程 | 1 次 HTTP 请求 | 2 次 HTTP 请求 1. OPTIONS 预检请求 2. 真实的请求 |
浏览器行为 | 自动在请求头中添加 Origin 字段,然后直接发出请求 |
1. 先自动发起 OPTIONS 预检请求询问服务器 2. 获得批准后,再发送真实请求 |
关键请求头 | Origin |
预检请求 (OPTIONS) 头: - Origin - Access-Control-Request-Method - Access-Control-Request-Headers (如果使用了自定义头) |
服务器关键响应头 | - Access-Control-Allow-Origin (必需)- Access-Control-Allow-Credentials (可选)- Access-Control-Expose-Headers (可选) |
预检响应头: - Access-Control-Allow-Origin (必需)- Access-Control-Allow-Methods (必需)- Access-Control-Allow-Headers (可选)- Access-Control-Max-Age (可选)- Access-Control-Allow-Credentials (可选)真实请求响应头与简单请求相同 |
主要目的 | 处理安全的、传统的跨域请求 | 处理可能有副作用的、更复杂的跨域请求,为服务器提供一层额外的安全控制 |
常见示例 | GET /api/data POST /api/submit (表单格式) |
PUT /api/update/1 DELETE /api/delete/1 POST /api/login (JSON格式,带Authorization头) |
补充说明:
- 预检请求:这是浏览器的一种安全机制,旨在防止对服务器产生未预期的副作用(例如,一个来自恶意网站的
DELETE
请求)。服务器通过响应预检请求,明确告知浏览器它允许哪些方法、头部和源。 Access-Control-Max-Age
:该头用于指定预检请求结果的缓存时间(单位:秒)。在有效期内,同一请求无需再次发起预检,从而提升性能。- 凭证请求:如果请求需要携带 Cookies 或 HTTP 认证信息,则服务器必须设置
Access-Control-Allow-Credentials: true
,并且Access-Control-Allow-Origin
不能为通配符*
,必须是具体的源。这对简单和非简单请求都适用。
2. 反向代理 (Reverse Proxy) - 开发阶段最常用、最安全的方法
在开发环境中,前端和服务端API之间存在跨域问题。我们可以在前端开发服务器和服务端API之间设置一个代理。前端将所有请求发送到同源的开发服务器,再由开发服务器代为转发给真正的API服务器。因为服务器之间通信没有同源策略限制,从而解决了跨域问题。
实现方式:
- Vite: 在
vite.config.js
中配置server.proxy
。 - Webpack (Vue CLI): 在
vue.config.js
中配置devServer.proxy
。
Vite 配置示例 (vite.config.js
):
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 8080, // 前端开发服务器端口
proxy: {
// 代理所有以 '/api' 开头的请求
'/api': {
target: 'http://api.true-server.com:3000', // 真实的后端API地址
changeOrigin: true, // 改变请求头中的Origin为目标地址,虚拟一个同源身份
rewrite: (path) => path.replace(/^\/api/, '') // (可选)重写请求路径,去掉/api前缀
}
}
}
})
前端代码调用示例:
// 前端现在请求的是同源的 /api/data,而不是 http://api.true-server.com:3000/data
axios.get('/api/data').then(response => {
console.log(response.data);
});
3. JSONP (JSON with Padding) - 古老的方法,仅限GET请求
利用 <script>
标签没有跨域限制的特点,通过动态创建<script>
标签,指定一个回调函数名作为参数传递给服务器,服务器返回一段以该回调函数包裹JSON数据的JS代码,前端在收到后立即执行。
例子:
前端代码:
function handleJSONPResponse(data) {
// 这里是回调函数,处理从服务器返回的数据
console.log('Received data:', data);
}
// 动态创建script标签
const script = document.createElement('script');
const url = 'http://api.other-site.com/data?callback=handleJSONPResponse';
script.src = url;
document.body.appendChild(script);
// 请求完成后移除script标签
script.onload = function() {
document.body.removeChild(script);
};
后端响应:
服务器接收到请求,发现参数中有 callback=handleJSONPResponse
,于是返回如下内容(不是JSON,而是一段可执行的JavaScript代码):
handleJSONPResponse({ "message": "This is data from JSONP!" });
缺点: 只支持GET方法,安全性较差,错误处理困难。
4. WebSocket - 双向通信协议,天然跨域
WebSocket是一种双向通信协议,在建立连接的HTTP握手阶段不受同源策略限制(但浏览器可能会在发起WebSocket连接时携带Origin头,服务器可以根据此决定是否接受连接)。
例子:
前端代码:
// 直接在客户端创建WebSocket连接,即使域名不同
const socket = new WebSocket('ws://api.other-site.com/ws');
socket.onopen = function(event) {
console.log('Connection established!');
socket.send('Hello Server!'); // 发送数据
};
socket.onmessage = function(event) {
console.log('Message from server:', event.data); // 接收数据
};
socket.onerror = function(error) {
console.error('WebSocket error:', error);
};
服务器端 (Node.js with ws
library):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws, request) {
// 这里可以检查 request.headers.origin 来决定是否允许连接
console.log('Client connected');
ws.on('message', function incoming(message) {
console.log('received: %s', message);
// 广播消息给所有客户端
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
});
5. 修改 document.domain - 适用于二级域名相同的情况
该方法只适用于两个页面拥有相同基础域名(例如 a.example.com
和 b.example.com
),且协议和端口都相同的情况。通过将两个页面的 document.domain
都设置为相同的基础域名(example.com
),浏览器就会认为它们同源,允许相互操作。
例子:
假设有两个页面:
http://shop.example.com/page.html
http://admin.example.com/other.html
在两个页面中均执行以下代码:
// 在两个页面的脚本中都设置
document.domain = 'example.com';
设置之后,这两个页面就可以相互访问对方的DOM了(例如使用window.parent
进行iframe通信)。
缺点: 局限性非常大,只能用于特定场景,且一个页面一旦设置了document.domain
,就无法再改回更具体的子域名。现代开发中已很少使用。
总结与选择
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
CORS | W3C标准,安全可控,支持所有HTTP方法 | 需要后端配合修改服务器配置 | 生产环境的终极解决方案 |
反向代理 | 前端无需修改代码,开发体验好,安全 | 主要用于开发环境,生产环境需Nginx等配合 | 开发阶段的首选方案 |
JSONP | 兼容老浏览器,无需后端特殊配置(需支持JSONP) | 仅支持GET,安全性差,错误处理难 | 临时解决老旧系统的GET请求跨域 |
WebSocket | 双向通信,实时性强,协议本身支持跨域 | 并非为普通API请求设计,协议较复杂 | 需要实时双向数据交换的场景(如聊天室、实时通知) |
document.domain | 可解决特定域名下的跨域问题 | 局限性极大(同基础域名、同协议、同端口) | 传统且域名规划有继承关系的旧项目 |
最佳实践:
- 开发阶段:使用 反向代理 (Vite/Webpack Proxy)。
- 生产环境:让后端配置 CORS。如果架构复杂,也可以在Nginx等网关层统一配置反向代理。
更多推荐
所有评论(0)