在前端开发中,"跨域"是一个绕不开的话题。无论是调用第三方 API,还是前后端分离项目中的本地联调,我们都可能遇到它。
一、什么是跨域问题?
要理解跨域,首先要知道什么是同源策略。
同源策略(Same-Origin Policy)是浏览器最核心也是最基本的安全功能。它规定了一个源(origin)的文档或脚本,不能读取或修改另一个源的文档属性。
所谓同源 ,指的是协议、域名、端口号三者完全相同。
举个例子:
|----------------------------|--------------------------------------|----------|-------------------------------------------|
| URL A | URL B | 是否同源 | 原因 |
| http://www.example.com/ | http://www.example.com/dir/page.html | 是 | 协议、域名、端口均相同 |
| http://www.example.com/ | https://www.example.com/ | 否 | 协议不同 (http vs https) |
| http://www.example.com/ | http://api.example.com/ | 否 | 域名不同 (www.example.com vs api.example.com) |
| http://www.example.com:80/ | http://www.example.com:8080/ | 否 | 端口不同 (80 vs 8080) |
跨域(Cross-Origin) 就是指一个源的文档或脚本试图请求另一个源的资源。当浏览器发现这是一个跨域请求,并且该请求不符合某些安全例外(如 CORS),浏览器就会出于安全考虑,阻止该请求或限制对响应的访问,这就是我们常说的跨域问题。
注:跨域问题本质上是浏览器的行为。服务器之间(如后端服务 A 调用后端服务 B)的 HTTP 请求不存在跨域问题。
二、前端为什么会有跨域问题?
同源策略的存在主要是为了保护用户信息安全,防止恶意网站窃取数据。想象一下,如果没有同源策略:
- 你登录了网上银行 https://mybank.com,浏览器保存了你的登录凭证(Cookie)。
- 你在不经意间访问了一个恶意网站 https://evil.com。
- 这个恶意网站的页面里有一段 JavaScript 代码,向 https://mybank.com/api/transfer?to=hacker\&amount=10000 发起了请求。
- 由于没有同源策略,浏览器会自动附上 mybank.com 的 Cookie,服务器会验证通过,执行转账操作。
- 你的钱就这样被悄无声息地转走了。
同源策略就是为了防止这种情况发生。它限制了 evil.com 的脚本读取 mybank.com 返回的响应数据,从而保护了用户信息。
三、如何解决跨域问题?
既然跨域是浏览器的一种安全限制,那么解决方案也必然围绕如何"告诉"浏览器这个跨域请求是安全的,或者如何绕过这个限制。以下是几种主流的解决方案:
1. CORS (Cross-Origin Resource Sharing) - 跨域资源共享
这是目前最推荐、最规范的解决方案。它是一种 HTTP 机制,允许服务器标示除了它自己以外的其他 origin(域、协议或端口),这样浏览器就可以访问加载这些资源。
CORS 的工作原理是:当浏览器发起一个跨域请求时,它会自动在请求头中添加一些信息(如 `Origin`),服务器根据这些信息判断是否允许该跨域请求,并在响应头中返回相应的许可信息。浏览器收到响应后,如果检查到服务器允许该请求,就不会报错。
简单请求 和 预检请求:
CORS 将请求分为两类:
- 简单请求:满足一定条件(如方法是 GET/POST/HEAD,Content-Type 为 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)的请求。浏览器会直接发送请求,并在响应头中检查 Access-Control-Allow-Origin。
- 非简单请求:如使用 PUT、DELETE 方法,或 Content-Type 为 application/json 的请求。浏览器会先发送一个 OPTIONS 方法的"预检请求"(Preflight Request)到服务器,询问是否允许该跨域请求。服务器确认允许后,浏览器才会发送真正的请求。
服务端配置示例 (Node.js + Express):
javascript
const express = require('express');
const app = express();
const port = 3001; // 后端服务端口
// 允许所有源跨域 (仅用于开发,生产环境应指定具体域名)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); // 或 'http://localhost:3000'
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
// 如果需要携带 Cookie
// res.header('Access-Control-Allow-Credentials', 'true');
// 处理预检请求
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
app.get('/api/data', (req, res) => {
res.json({ message: '这是来自跨域服务器的数据!' });
});
app.listen(port, () => {
console.log(`后端服务运行在 http://localhost:${port}`);
});
前端调用示例 (使用 Fetch API):
javascript
// 假设前端运行在 http://localhost:3000
fetch('http://localhost:3001/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
企业开发实践:
在生产环境中,Access-Control-Allow-Origin 不应设置为 *,而应设置为前端应用的域名,如 https://www.myapp.com,以增强安全性。通常,这些 CORS 头会在反向代理(如 Nginx)或 API 网关层面统一配置。
2. 代理服务器
代理服务器是解决跨域问题的"万能钥匙"。其核心思想是:浏览器有跨域限制,但服务器之间没有。
原理:
- 前端应用向同源的代理服务器发送请求。
- 代理服务器接收到请求后,将其转发给真正的目标后端服务器(跨域)。
- 后端服务器将响应返回给代理服务器。
- 代理服务器再将响应返回给前端应用。
对于前端来说,它始终是在和同源的代理服务器通信,因此不存在跨域问题。
企业开发实践:
开发环境: 前端构建工具(如 Vite, Webpack)通常内置了代理功能。Vite ( vite.config.js ):
javascript
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3001', // 真实后端服务地址
changeOrigin: true, // 修改请求头中的 Origin 为目标地址
// 可选:重写路径
// rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});
注意:前端代码中请求 /api/data,Vite 开发服务器会自动将其代理到 http://localhost:3001/api/data。
Webpack (vue.config.js 或 webpack.config.js):
javascript
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
pathRewrite: { '^/api': '' } // 重写路径,移除 /api
}
}
}
};
生产环境: 通常使用 Nginx 作为反向代理。
Nginx 配置示例 (nginx.conf):
javascript
server {
listen 80;
server_name www.myapp.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; # SPA 路由支持
}
location /api/ {
proxy_pass http://backend-server:3001/; # 后端服务地址
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;
}
}
这样,前端访问 www.myapp.com/api/data,Nginx 会将其代理到 http://backend-server:3001/data。
3. JSONP (JSON with Padding) - 仅支持 GET 请求
JSONP 是一种比较"古老"的跨域解决方案,它利用了 <script> 标签不受同源策略限制的特性。
原理:
- 前端定义一个回调函数,如 handleResponse 。
- 创建一个 <script> 标签,其 src 指向跨域 API,并将回调函数名作为参数传递,如 http://api.example.com/data?callback=handleResponse 。
- 服务器接收到请求后,将数据包裹在回调函数中返回,如 handleResponse({"name": "Alice", "age": 25}); 。
- 浏览器接收到并执行这段 JavaScript,从而调用前端定义的回调函数,并将数据作为参数传入。
服务端示例 (Node.js + Express):
javascript
const express = require('express');
const app = express();
const port = 3002;
app.get('/api/jsonp', (req, res) => {
const callbackName = req.query.callback;
const data = { message: '这是 JSONP 返回的数据!' };
const script = `${callbackName}(${JSON.stringify(data)})`;
res.send(script);
});
app.listen(port, () => {
console.log(`JSONP 服务运行在 http://localhost:${port}`);
});
前端调用示例:
javascript
function handleResponse(data) {
console.log('JSONP 响应:', data);
}
function loadJSONP() {
const script = document.createElement('script');
script.src = 'http://localhost:3002/api/jsonp?callback=handleResponse';
document.body.appendChild(script);
// 可选:请求完成后移除 script 标签
script.onload = () => {
document.body.removeChild(script);
};
}
loadJSONP();
缺点:
- 只支持 GET 请求。
- 安全性较低,容易受到 XSS 攻击(如果服务器对回调函数名过滤不严)。
- 错误处理困难。
现在,CORS 已经普及,JSONP 逐渐被淘汰,但在与一些只支持 JSONP 的老旧第三方服务交互时,可能还会用到。
4. 其他方案(了解即可)
- WebSocket: WebSocket 协议不受同源策略限制,可以进行跨域通信。
- postMessage: 用于不同窗口(iframe、popup)之间的安全跨域通信。
- document.domain: 只适用于主域相同、子域不同的情况(如 a.example.com 和 b.example.com ),现在已不推荐使用。
四、方案的对比与选择
- 跨域是浏览器安全策略(同源策略)导致的问题,目的是保护用户数据。
- CORS 是现代 Web 开发解决跨域问题的标准方案,需要后端服务器设置特定的 HTTP 响应头。
- 代理服务器是一种非常实用的绕过方案,尤其在开发环境和需要统一 API 网关的生产环境中。
- JSONP 是一种过时的技术,仅在特定兼容性需求下考虑。