跨域是 Web 开发中绕不开的话题。只要页面、脚本和接口不在同一个“源”(协议 + 域名 + 端口完全相同),浏览器就会基于同源策略,阻止脚本读取另一个源的响应内容。这本质上是一种安全机制,但也给前后端分离开发带来诸多不便。下面逐一剖析最常见的跨域解决方案,每种方案的原理、用法和适用场景都给出清晰说明。
16.4.1 JSONP(JSON with Padding)
JSONP 是最早广泛应用于浏览器端的跨域方案,利用 <script> 标签不受同源策略限制的特性。
原理
在页面上动态创建一个 <script> 标签,将跨域请求的 URL 设置为它的 src,同时约定一个回调函数名作为参数。服务端收到请求后,返回一段 JavaScript 代码:调用该回调函数,并把真正的数据作为参数传入。浏览器收到响应后立即执行这段代码,从而将数据传递给页面逻辑。
前端示例
function handleResponse(data) {
console.log('获取到跨域数据:', data);
}
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);
服务器返回
handleResponse({ name: 'Alice', score: 95 });
优点与局限
- 优点:兼容性好,支持老旧浏览器;实现简单。
- 缺点:只支持 GET 请求;不能设置自定义请求头;依赖第三方接口配合输出 JSONP 格式;存在安全风险(可能被注入恶意代码)。
现代项目已较少使用,但在一些遗留系统或简单数据接入场景中仍能见到。
16.4.2 CORS(跨域资源共享)
CORS 是 W3C 标准,也是目前解决跨域问题的官方方案。它通过服务器在响应中添加一系列 HTTP 头,明确告知浏览器“允许哪些源来访问本资源”。浏览器自动根据响应头决定是否放行。
关键响应头
Access-Control-Allow-Origin:指定允许的来源,如*表示所有源(不能携带 Cookie),或设置为具体域名。Access-Control-Allow-Methods:允许的 HTTP 方法,如GET, POST, PUT, DELETE。Access-Control-Allow-Headers:允许的自定义请求头。Access-Control-Allow-Credentials:是否允许携带身份凭证(Cookie、HTTP 认证等),设置为true时Allow-Origin不能为*。
简单请求与预检请求
- 简单请求:方法为 GET/HEAD/POST,Content-Type 限于特定三种值,不触发预检。浏览器直接发请求并判断响应头。
- 非简单请求(如 PUT、DELETE,或自定义请求头),浏览器会先发一个
OPTIONS请求(称为预检),确认服务器允许后才发送真正的请求。
服务器设置示例(Node.js + Express)
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://example.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
前端无需特殊处理,只需按正常方式发送请求即可。浏览器会自动完成 CORS 流程。
注意:CORS 解决的是浏览器端的限制,后端之间的调用没有跨域问题。
16.4.3 代理(开发环境代理)
在前后端分离开发中,前端通常运行在 localhost:3000,后端接口在 localhost:8080,不同端口即构成跨域。开发环境下,前端构建工具(如 Webpack DevServer、Vite)提供了代理功能,将以约定路径开头的请求转发到后端服务器,从而绕过浏览器的同源策略。
原理
浏览器请求始终发往同源的前端开发服务器,开发服务器内部将请求转发给真正的后端,并将后端返回的数据返回给浏览器。整个过程对浏览器而言,请求始终在同一源下,不存在跨域。
Vite 配置示例(vite.config.js)
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
使用场景
- 本地开发阶段,所有
/api/xxx的请求会被转发到http://localhost:8080/xxx。 - 生产环境下通常用 Nginx 实现相同目的,但功能逻辑一致。
代理方案只适用于开发环境,上线后仍需要 CORS 或 Nginx 反向代理来统一处理跨域。
16.4.4 Nginx 反向代理
在线上生产环境中,最常用的跨域解决方案是使用 Nginx 作为反向代理服务器。它将前端静态资源和后端 API 统一代理到同一个域名下,从浏览器角度看,所有请求都在同一个源内。
典型配置
server {
listen 80;
server_name example.com;
# 前端静态资源
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# API 接口反向代理
location /api/ {
proxy_pass http://backend-server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
原理
浏览器访问 https://example.com 加载页面,页面中请求 /api/user 的接口。由于页面和 API 在同一个域名下,不涉及跨域。Nginx 收到 /api/ 开头的请求后,将其转发到真正的后端 http://backend-server:8080/,再把后端返回的内容原样返回给浏览器。
优势
- 彻底消除前端跨域烦恼,无需后端修改 CORS 配置。
- 支持负载均衡、静态资源缓存、HTTPS 终结等高级功能。
- 生产环境标准实践,稳定高效。
16.4.5 postMessage
当需要在不同源的两个窗口(或 iframe)之间传递数据时,以上网络请求方案就无能为力了。postMessage 是 HTML5 提供的安全跨文档通信 API,允许不同源的页面间进行有限制的消息传递。
使用场景
- 页面内嵌的 iframe 需要与父页面交互(例如第三方支付结果通知、广告数据汇报)。
- 多窗口或多标签页之间的通信。
发送方(父页面)
const iframe = document.getElementById('myFrame');
iframe.contentWindow.postMessage(
{ type: 'hello', content: '来自父页面的消息' },
'https://child.example.com' // 目标源,设置为具体域名
);
接收方(iframe 内部)
window.addEventListener('message', (event) => {
// 必须校验消息来源,防止被恶意站点利用
if (event.origin !== 'https://parent.example.com') return;
console.log('收到消息:', event.data);
// 可回复消息
event.source.postMessage('已收到', event.origin);
});
安全注意
- 始终校验
event.origin,避免处理来自非信任源的消息。 - 避免通过
postMessage传递敏感数据,或使用*作为目标源,这会带来安全隐患。
16.4.6 其他辅助方案(简述)
- WebSocket:协议本身不实行同源策略,但服务器端可以校验
Origin头,可作为一种实时双向跨域通信手段。 - document.domain + iframe:仅限主域相同、子域不同的情况,且已逐步被弃用,不推荐新项目使用。
- window.name:古老的黑科技,利用 window.name 在页面跳转时仍然保持的特性来传递数据,不推荐使用。
小结:如何选择跨域方案
| 方案 | 适用场景 | 是否推荐 |
|------|----------|----------|
| JSONP | 老旧浏览器、简单 GET 数据获取 | 不推荐,除非维护旧系统 |
| CORS | 标准 API 跨域访问,前后端分离上线环境 | 推荐,现代标配 |
| 开发环境代理 | 本地开发时解决前后端跨域 | 推荐,日常开发必备 |
| Nginx 反向代理 | 生产环境统一域名、统一端口 | 推荐,生产最佳实践 |
| postMessage | iframe 或不同窗口之间的安全通信 | 特定场景专用 |
每一种方案都有其历史背景和适用边界,理解它们的作用原理,能让你在面对实际项目时快速做出最优选择。