跨域通信解决方案完全指南
「No 'Access-Control-Allow-Origin' header is present on the requested resource.」——几乎每个前端开发者都见过这个令人头疼的错误。
跨域问题源于浏览器的同源策略,这是一种重要的安全机制。理解它的原理和各种解决方案,是前端开发的必备技能。
本文将系统讲解跨域的本质、各种解决方案的原理和适用场景,以及在实际项目中的配置方法。
理解同源策略
什么是同源
同源是指两个 URL 的协议、域名、端口完全相同。
https://example.com:443/page
协议 域名 端口 路径
同源判断示例:
| URL | 与 https://example.com 比较 | 结果 |
|---|---|---|
https://example.com/page | 同源 | ✅ |
https://example.com:443/api | 同源(443 是 https 默认端口) | ✅ |
http://example.com | 协议不同 | ❌ |
https://api.example.com | 域名不同(子域名也算) | ❌ |
https://example.com:8080 | 端口不同 | ❌ |
https://example.org | 域名不同 | ❌ |
同源策略限制什么
受限制的操作:
- AJAX 请求:不能读取跨域响应
- DOM 访问:不能操作跨域 iframe 的 DOM
- Cookie/Storage:不能读取跨域的存储数据
不受限制的资源加载:
<script src="..."><link href="..."><img src="..."><video>、<audio><iframe>(可加载,但不能访问其 DOM)
为什么需要同源策略
防止恶意网站窃取数据:
假设没有同源策略:
- 你登录了银行网站
bank.com,有一个有效的 session - 你访问了恶意网站
evil.com evil.com的 JS 可以直接请求bank.com/api/account- 请求会自动携带你的 cookie
- 恶意网站获取了你的银行账户信息
同源策略阻止了第 3 步的跨域请求,保护了用户数据安全。
CORS:标准跨域解决方案
CORS 的工作原理
CORS(Cross-Origin Resource Sharing) 是 W3C 标准,允许服务器声明哪些源可以访问其资源。
简单请求流程:
浏览器 服务器
| |
| -------- 请求 --------> |
| Origin: https://example.com |
| |
| <-------- 响应 -------- |
| Access-Control-Allow-Origin: |
| https://example.com |
| |
浏览器检查响应头,允许则正常处理
预检请求流程(复杂请求):
浏览器 服务器
| |
| ------ OPTIONS 预检 ----> |
| Origin: https://example.com |
| Access-Control-Request-Method:|
| POST |
| Access-Control-Request-Headers:|
| Content-Type, Authorization |
| |
| <------ 预检响应 ------- |
| Access-Control-Allow-Origin: |
| https://example.com |
| Access-Control-Allow-Methods: |
| GET, POST, PUT |
| Access-Control-Allow-Headers: |
| Content-Type, Authorization |
| Access-Control-Max-Age: 86400 |
| |
预检通过后,发送实际请求
| |
| -------- 实际请求 -----> |
| <-------- 实际响应 ----- |
简单请求 vs 预检请求
简单请求条件(必须同时满足):
- 方法:
GET、HEAD、POST - Content-Type:
text/plainmultipart/form-dataapplication/x-www-form-urlencoded
- 不包含自定义请求头
触发预检的情况:
- 使用
PUT、DELETE、PATCH方法 - Content-Type 为
application/json - 包含自定义请求头(如
Authorization)
CORS 响应头详解
| 响应头 | 说明 | 示例值 |
|---|---|---|
Access-Control-Allow-Origin | 允许的源 | https://example.com 或 * |
Access-Control-Allow-Methods | 允许的方法 | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | 允许的请求头 | Content-Type, Authorization |
Access-Control-Allow-Credentials | 是否允许携带凭证 | true |
Access-Control-Expose-Headers | 允许前端访问的响应头 | X-Custom-Header |
Access-Control-Max-Age | 预检结果缓存时间(秒) | 86400 |
服务端 CORS 配置
Node.js (Express):
const cors = require('cors')
// 简单配置:允许所有源
app.use(cors())
// 详细配置
app.use(cors({
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}))
// 动态判断源
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = ['https://example.com', 'https://app.example.com']
// 允许无 origin 的请求(如本地文件、Postman)
if (!origin) return callback(null, true)
if (allowedOrigins.includes(origin)) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
}
}))
Nginx:
server {
location /api/ {
# 设置 CORS 头
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' '86400' always;
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' '86400';
add_header 'Content-Length' '0';
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
proxy_pass http://backend;
}
}
Nuxt 3 (Nitro):
// server/middleware/cors.ts
export default defineEventHandler((event) => {
const origin = getHeader(event, 'origin')
const allowedOrigins = ['https://example.com', 'https://app.example.com']
if (origin && allowedOrigins.includes(origin)) {
setHeader(event, 'Access-Control-Allow-Origin', origin)
setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type, Authorization')
setHeader(event, 'Access-Control-Allow-Credentials', 'true')
setHeader(event, 'Access-Control-Max-Age', '86400')
}
// 处理预检请求
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204
event.node.res.end()
return
}
})
代理转发
当你无法控制后端 CORS 配置时,可以通过代理绕过跨域限制。
原理
浏览器 ---> 同源代理服务器 ---> 目标 API 服务器
(无跨域问题) (服务器间无同源限制)
Vite 开发代理
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// 多个代理
'/auth': {
target: 'https://auth.example.com',
changeOrigin: true
}
}
}
})
前端请求:
// 实际发送到 https://api.example.com/users
fetch('/api/users')
// 实际发送到 https://auth.example.com/login
fetch('/auth/login')
Nuxt 代理配置
开发环境代理:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
devProxy: {
'/api/': {
target: 'https://api.example.com',
changeOrigin: true
}
}
}
})
生产环境代理(通过路由):
// server/api/proxy/[...path].ts
export default defineEventHandler(async (event) => {
const path = getRouterParam(event, 'path')
const targetUrl = `https://api.example.com/${path}`
// 转发请求
return proxyRequest(event, targetUrl, {
headers: {
'X-Forwarded-For': getHeader(event, 'x-forwarded-for') || ''
}
})
})
Nginx 反向代理
server {
listen 80;
server_name example.com;
# 前端静态文件
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
# API 代理
location /api/ {
proxy_pass https://api.example.com/;
proxy_set_header Host api.example.com;
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;
}
}
postMessage:跨文档通信
window.postMessage() 用于在不同源的窗口间安全通信。
使用场景
- iframe 与父页面通信
- 主窗口与弹出窗口通信
- 不同标签页间通信(需通过 SharedWorker 或 localStorage)
基本用法
发送消息:
// 向 iframe 发送消息
const iframe = document.getElementById('myIframe')
iframe.contentWindow.postMessage(
{ type: 'greeting', data: 'Hello from parent' },
'https://child.example.com' // 指定目标源,安全考虑
)
// 向父窗口发送消息(在 iframe 内)
window.parent.postMessage(
{ type: 'response', data: 'Hello from child' },
'https://parent.example.com'
)
接收消息:
window.addEventListener('message', (event) => {
// 安全检查:验证消息来源
if (event.origin !== 'https://trusted.example.com') {
console.warn('Untrusted origin:', event.origin)
return
}
// 处理消息
const { type, data } = event.data
switch (type) {
case 'greeting':
console.log('Received greeting:', data)
break
case 'request':
handleRequest(data, event.source)
break
}
})
function handleRequest(data, source) {
// 处理请求并回复
const result = processData(data)
source.postMessage(
{ type: 'response', data: result },
event.origin
)
}
封装通信模块
// utils/postMessage.ts
interface MessagePayload {
type: string
id: string
data: any
}
type MessageHandler = (data: any) => any | Promise<any>
class CrossOriginMessenger {
private handlers = new Map<string, MessageHandler>()
private pendingRequests = new Map<string, {
resolve: (value: any) => void
reject: (error: any) => void
}>()
private targetOrigin: string
private targetWindow: Window
constructor(targetWindow: Window, targetOrigin: string) {
this.targetWindow = targetWindow
this.targetOrigin = targetOrigin
window.addEventListener('message', this.handleMessage.bind(this))
}
// 注册消息处理器
on(type: string, handler: MessageHandler) {
this.handlers.set(type, handler)
}
// 发送消息(无需响应)
send(type: string, data: any) {
const payload: MessagePayload = {
type,
id: this.generateId(),
data
}
this.targetWindow.postMessage(payload, this.targetOrigin)
}
// 发送请求(等待响应)
async request<T>(type: string, data: any): Promise<T> {
const id = this.generateId()
const payload: MessagePayload = { type, id, data }
return new Promise((resolve, reject) => {
this.pendingRequests.set(id, { resolve, reject })
this.targetWindow.postMessage(payload, this.targetOrigin)
// 超时处理
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id)
reject(new Error('Request timeout'))
}
}, 30000)
})
}
private async handleMessage(event: MessageEvent) {
if (event.origin !== this.targetOrigin) return
const { type, id, data } = event.data as MessagePayload
// 处理响应
if (type === 'response' && this.pendingRequests.has(id)) {
const { resolve } = this.pendingRequests.get(id)!
this.pendingRequests.delete(id)
resolve(data)
return
}
// 处理请求
const handler = this.handlers.get(type)
if (handler) {
try {
const result = await handler(data)
event.source?.postMessage(
{ type: 'response', id, data: result },
{ targetOrigin: event.origin }
)
} catch (error) {
event.source?.postMessage(
{ type: 'response', id, data: { error: error.message } },
{ targetOrigin: event.origin }
)
}
}
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
destroy() {
window.removeEventListener('message', this.handleMessage)
}
}
export { CrossOriginMessenger }
使用示例:
// 父页面
const iframe = document.getElementById('myIframe') as HTMLIFrameElement
const messenger = new CrossOriginMessenger(
iframe.contentWindow!,
'https://child.example.com'
)
// 发送请求
const userInfo = await messenger.request('getUserInfo', { userId: 123 })
// 接收事件
messenger.on('childEvent', (data) => {
console.log('Received from child:', data)
})
WebSocket 跨域
WebSocket 不受同源策略限制,但需要服务端配合。
WebSocket 连接
// 直接连接跨域 WebSocket
const ws = new WebSocket('wss://api.example.com/ws')
ws.onopen = () => {
console.log('WebSocket connected')
ws.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }))
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('Received:', data)
}
ws.onerror = (error) => {
console.error('WebSocket error:', error)
}
服务端验证
虽然浏览器不阻止 WebSocket 跨域连接,但服务端应该验证 Origin:
// Node.js (ws 库)
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8080 })
wss.on('connection', (ws, request) => {
const origin = request.headers.origin
// 验证来源
const allowedOrigins = ['https://example.com', 'https://app.example.com']
if (!allowedOrigins.includes(origin)) {
ws.close(1008, 'Origin not allowed')
return
}
ws.on('message', (message) => {
// 处理消息
})
})
其他跨域方案
JSONP(已过时)
JSONP 利用 <script> 标签不受同源限制的特性,只支持 GET 请求。
// 定义回调函数
window.handleResponse = (data) => {
console.log('Received:', data)
delete window.handleResponse
}
// 动态创建 script 标签
const script = document.createElement('script')
script.src = 'https://api.example.com/data?callback=handleResponse'
document.body.appendChild(script)
服务端返回:
handleResponse({"name": "John", "age": 30})
注意:JSONP 存在安全风险,现代应用应使用 CORS。
document.domain(同主域)
对于同一主域下的子域名,可以通过设置 document.domain 实现通信:
// https://a.example.com
document.domain = 'example.com'
// https://b.example.com
document.domain = 'example.com'
// 现在两个页面可以互相访问 DOM
限制:只能用于同主域的子域名。
携带凭证的跨域请求
Cookie 和认证信息
默认情况下,跨域请求不会携带 Cookie。如需携带:
前端配置:
// Fetch
fetch('https://api.example.com/data', {
credentials: 'include' // 携带 cookie
})
// Axios
axios.get('https://api.example.com/data', {
withCredentials: true
})
服务端配置:
// 必须设置 credentials 为 true
res.setHeader('Access-Control-Allow-Credentials', 'true')
// 注意:当 credentials 为 true 时,Origin 不能是 *
res.setHeader('Access-Control-Allow-Origin', 'https://example.com')
常见问题
问题 1:Origin 设置为 * 时无法携带凭证
错误:The value of the 'Access-Control-Allow-Origin' header must not be
the wildcard '*' when the request's credentials mode is 'include'.
解决:使用具体的 Origin 值
问题 2:Cookie 未设置 SameSite
// 服务端设置 Cookie 时
res.cookie('token', value, {
httpOnly: true,
secure: true,
sameSite: 'None' // 跨站请求需要设置为 None
})
调试跨域问题
浏览器开发者工具
Network 面板检查点:
- 查看请求头中的
Origin - 查看响应头中的
Access-Control-Allow-Origin - 检查是否有 OPTIONS 预检请求
- 查看预检请求的响应状态
常见错误排查
错误 1:No 'Access-Control-Allow-Origin' header
原因:服务端未返回 CORS 头
检查:确认服务端已配置 CORS
错误 2:CORS header contains multiple values
原因:CORS 头被设置了多次
检查:Nginx 和后端是否都配置了 CORS
解决:只在一处配置
错误 3:Preflight response is not successful
原因:OPTIONS 预检请求失败
检查:
- 服务端是否处理 OPTIONS 请求
- 是否返回了正确的 CORS 头
- 防火墙/网关是否拦截了 OPTIONS
错误 4:Credential is not supported if CORS origin is '*'
原因:携带凭证时 Origin 不能为 *
解决:设置具体的 Origin 值
调试脚本
// 在控制台运行,测试 CORS 配置
async function testCors(url) {
console.log('Testing CORS for:', url)
try {
// 简单请求
const simpleResponse = await fetch(url)
console.log('Simple request headers:',
[...simpleResponse.headers.entries()])
// 预检请求
const preflightResponse = await fetch(url, {
method: 'OPTIONS',
headers: {
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'Content-Type'
}
})
console.log('Preflight response headers:',
[...preflightResponse.headers.entries()])
} catch (error) {
console.error('CORS test failed:', error)
}
}
testCors('https://api.example.com/test')
最佳实践
安全建议
- 明确指定允许的源
// ❌ 避免 res.setHeader('Access-Control-Allow-Origin', '*')
// ✅ 推荐 res.setHeader('Access-Control-Allow-Origin', 'https://example.com')
2. **限制允许的方法和头**
```javascript
// 只开放必要的方法
res.setHeader('Access-Control-Allow-Methods', 'GET, POST')
// 只开放必要的请求头
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
- 验证 postMessage 来源
window.addEventListener('message', (event) => { if (event.origin !== 'https://trusted.example.com') { return } // ... })
方案选择指南
| 场景 | 推荐方案 |
|---|---|
| 标准 API 调用 | CORS(服务端配置) |
| 无法修改服务端 | 代理转发 |
| iframe 通信 | postMessage |
| 实时通信 | WebSocket |
| 开发环境 | Vite/Webpack 代理 |
检查清单
- 确认是否真的需要跨域(同源请求无需处理)
- 优先考虑 CORS(标准、安全)
- 开发环境配置代理
- 生产环境统一域名或配置 CORS
- 携带凭证时使用具体 Origin
- postMessage 验证消息来源
- 定期审查 CORS 配置安全性
总结
跨域问题的本质是浏览器的安全策略。理解同源策略的目的,才能正确选择解决方案:
- CORS:标准方案,需要服务端配合
- 代理:无法修改服务端时的替代方案
- postMessage:跨文档通信的专用方案
- WebSocket:实时通信场景
核心原则:安全第一。不要为了方便而开放不必要的跨域权限,明确指定允许的源、方法和请求头,才能在保证功能的同时确保安全。


