跨域问题详解
一、什么是跨域问题?
跨域(Cross-Origin) 是指浏览器在发起网络请求时,请求的目标地址与当前页面的地址在以下任意一个维度上存在差异:
| 维度 | 示例(当前页面) | 示例(请求目标) | 是否跨域 |
|---|---|---|---|
| 协议不同 | http://example.com | https://example.com | ✅ 跨域 |
| 域名不同 | http://a.com | http://b.com | ✅ 跨域 |
| 子域名不同 | http://www.example.com | http://api.example.com | ✅ 跨域 |
| 端口不同 | http://example.com:3000 | http://example.com:8080 | ✅ 跨域 |
| 完全相同 | http://example.com:3000 | http://example.com:3000 | ❌ 同源 |
同源的定义:协议 + 域名 + 端口 三者完全一致,才算同源。
二、为什么会有跨域问题?
2.1 同源策略(Same-Origin Policy)
跨域问题的根源是浏览器的 同源策略,这是浏览器最核心的安全机制之一,由 Netscape 在 1995 年提出。
同源策略的核心限制:
- 无法读取非同源页面的 Cookie、LocalStorage、IndexedDB
- 无法操作非同源页面的 DOM
- 无法向非同源地址发送 AJAX 请求(请求会被浏览器拦截)
2.2 为什么需要同源策略?
试想没有同源策略的世界:
1. 用户登录了 bank.com,浏览器保存了登录 Cookie
2. 用户访问了恶意网站 evil.com
3. evil.com 的脚本直接向 bank.com 发送请求,并携带用户的 Cookie
4. 恶意脚本成功获取用户的银行账户信息!同源策略正是为了防止这类 CSRF(跨站请求伪造) 和 XSS(跨站脚本攻击) 而存在的。
2.3 注意:跨域是浏览器行为
⚠️ 重要:跨域限制只存在于浏览器环境中。服务器之间的请求、Postman、curl 等工具不受同源策略限制。
三、如何解决跨域问题?
方案一:CORS(跨域资源共享)— 推荐方案
CORS(Cross-Origin Resource Sharing) 是目前最标准、最主流的跨域解决方案,由服务端设置响应头来授权跨域访问。
简单请求 vs 预检请求
简单请求(满足以下所有条件):
- 请求方法为
GET、POST、HEAD - Content-Type 为
text/plain、multipart/form-data、application/x-www-form-urlencoded
浏览器直接发送请求,并在请求头中带上 Origin 字段。
预检请求(Preflight)(不满足简单请求条件):
- 浏览器先发送一个
OPTIONS请求询问服务器是否允许 - 服务器确认后,再发送真正的请求
服务端配置示例
Node.js / Express:
const cors = require('cors');
app.use(cors({
origin: 'https://your-frontend.com', // 允许的源
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // 允许携带 Cookie
}));Nginx:
location /api/ {
add_header Access-Control-Allow-Origin "https://your-frontend.com";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
add_header Access-Control-Allow-Credentials "true";
if ($request_method = OPTIONS) {
return 204;
}
}Spring Boot(Java):
@CrossOrigin(origins = "https://your-frontend.com")
@RestController
public class ApiController {
// ...
}关键响应头说明
| 响应头 | 说明 |
|---|---|
Access-Control-Allow-Origin | 允许的源,* 表示所有源(不能与 credentials 同时使用) |
Access-Control-Allow-Methods | 允许的请求方法 |
Access-Control-Allow-Headers | 允许的请求头 |
Access-Control-Allow-Credentials | 是否允许携带 Cookie |
Access-Control-Max-Age | 预检请求的缓存时间(秒) |
方案二:代理服务器(Proxy)
通过在同源的服务器上设置代理,将请求转发到目标服务器,绕过浏览器的同源限制。
浏览器 → 同源代理服务器 → 目标服务器
(无跨域限制)开发环境:Vite 配置代理
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://backend-server.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}开发环境:webpack-dev-server 配置代理
// webpack.config.js
devServer: {
proxy: {
'/api': {
target: 'http://backend-server.com',
changeOrigin: true
}
}
}✅ 代理方案在开发环境中非常常用,生产环境可使用 Nginx 反向代理。
方案三:JSONP(历史方案,了解即可)
利用 <script> 标签不受同源策略限制的特性,通过动态创建 script 标签来获取数据。
function handleData(data) {
console.log('收到数据:', data);
}
const script = document.createElement('script');
script.src = 'http://api.example.com/data?callback=handleData';
document.body.appendChild(script);缺点:
- 只支持
GET请求 - 存在 XSS 安全风险
- 需要服务端配合返回特定格式
❌ 现代开发中已基本被 CORS 取代,不推荐使用。
方案四:postMessage(跨窗口通信)
适用于 iframe 与父页面、多个窗口/标签页之间的跨域通信场景。
// 发送方(页面 A)
const iframe = document.getElementById('my-iframe');
iframe.contentWindow.postMessage('Hello from A', 'https://page-b.com');
// 接收方(页面 B)
window.addEventListener('message', (event) => {
if (event.origin !== 'https://page-a.com') return; // 安全验证
console.log('收到消息:', event.data);
});方案五:WebSocket
WebSocket 协议不受同源策略限制,可以跨域建立连接(服务端可验证 Origin 头)。
const ws = new WebSocket('wss://api.other-domain.com/socket');
ws.onopen = () => ws.send('Hello');
ws.onmessage = (e) => console.log(e.data);四、各方案对比
| 方案 | 适用场景 | 难度 | 安全性 | 推荐度 |
|---|---|---|---|---|
| CORS | 所有 HTTP 请求 | ⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 代理服务器 | 开发/生产环境 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| JSONP | 仅 GET 请求(旧系统) | ⭐⭐ | ⭐⭐ | ⭐ |
| postMessage | 跨窗口/iframe 通信 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| WebSocket | 实时双向通信 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
五、常见问题与排查
Q1:设置了 Access-Control-Allow-Origin: *,但还是跨域报错?
原因:当请求需要携带 Cookie(credentials: 'include')时,* 无效,必须指定具体的源。
// ❌ 错误
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// ✅ 正确
Access-Control-Allow-Origin: https://your-frontend.com
Access-Control-Allow-Credentials: trueQ2:OPTIONS 预检请求返回 404 或 405?
服务端没有处理 OPTIONS 请求方法,需要添加对应的路由或在 Nginx 中配置返回 204。
Q3:开发环境正常,生产环境跨域?
通常是生产环境的 Nginx/Apache 没有配置 CORS 响应头,或者配置的域名与实际前端域名不一致。
Q4:跨域请求成功了,但 Cookie 没有携带?
需要同时满足三个条件:
- 前端请求设置
credentials: 'include' - 服务端响应头包含
Access-Control-Allow-Credentials: true - 服务端
Access-Control-Allow-Origin必须是具体域名,不能是*
六、安全建议
- ✅ 生产环境中
Access-Control-Allow-Origin应指定具体域名,避免使用* - ✅ 对
Origin进行白名单验证,而不是直接将请求中的Origin原样返回 - ✅ 合理设置
Access-Control-Max-Age,减少预检请求次数(建议 600~86400 秒) - ✅ 最小化
Access-Control-Allow-Methods和Access-Control-Allow-Headers,按需开放 - ❌ 不要在开发图省事的情况下,将生产环境配置为
Allow-Origin: *
七、总结
跨域问题
├── 根源:浏览器同源策略(安全机制)
├── 触发条件:协议 / 域名 / 端口 任一不同
└── 解决方案
├── CORS(主流,服务端配置响应头)
├── 代理服务器(开发/生产环境均可用)
├── JSONP(已过时,仅 GET)
├── postMessage(跨窗口通信专用)
└── WebSocket(实时通信场景)💡 推荐实践:生产环境使用 CORS + Nginx 反向代理 的组合方案,开发环境使用脚手架自带的 Proxy 代理,简洁安全。
