最后更新:2026-02-22

跨域应该是前端面试被问烂了的话题,工作里也几乎每个项目都会遇到。但很多人只记得”配 CORS 头”或者”用代理”,问起背后的原因,往往说不清楚。

我把这个问题从头梳理一遍。

为什么会有跨域

浏览器有一个安全机制叫同源策略(Same-Origin Policy)。「同源」的定义是:协议、域名、端口三者完全相同。

对比 同源?
http://a.com vs https://a.com ❌ 协议不同
http://a.com vs http://b.com ❌ 域名不同
http://a.com:80 vs http://a.com:3000 ❌ 端口不同
http://a.com/page1 vs http://a.com/page2 ✅ 同源

同源策略规定:非同源的页面之间,不能互相读取对方的 Cookie、LocalStorage,也不能操作对方的 DOM,更重要的是——不能发送 Ajax 请求到非同源的地址

这是浏览器对用户的保护,防止恶意网站偷你的数据。

CORS:服务端开门

CORS(Cross-Origin Resource Sharing,跨域资源共享)是目前最主流的跨域解决方案。本质是:服务端在响应头里声明「我允许谁来访问」。

简单请求

满足以下条件的是简单请求:

  • 方法是 GET、POST 或 HEAD
  • Content-Type 是 text/plainapplication/x-www-form-urlencodedmultipart/form-data

简单请求只需要服务端设置一个响应头:

1
2
3
Access-Control-Allow-Origin: https://your-frontend.com
# 或者允许所有来源(不安全,生产环境慎用)
Access-Control-Allow-Origin: *

预检请求(Preflight)

不满足简单请求条件的(比如用了 PUT/DELETE,或者 Content-Type 是 application/json),浏览器会先发一个 OPTIONS 请求问服务器「你允许我这么请求吗」,服务器回答允许后,才发真正的请求。

服务端需要处理 OPTIONS 请求:

1
2
3
4
Access-Control-Allow-Origin: https://your-frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400 # 预检结果缓存时间(秒)

Node.js/Express 里通常用 cors 中间件:

1
2
3
4
5
6
7
8
const cors = require('cors')

app.use(cors({
origin: 'https://your-frontend.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true // 允许携带 Cookie
}))

注意:如果前端请求需要带 Cookie(withCredentials: true),服务端的 Access-Control-Allow-Origin 不能设为 *,必须指定具体域名,同时要加 Access-Control-Allow-Credentials: true

开发环境:代理转发

CORS 需要改服务端,但有时候后端不是你负责的,或者你在调第三方接口,改不了服务端。这时候开发环境最常用的方案是代理

代理的原理:让同域的本地开发服务器转发请求,浏览器看到的是同域请求,不触发跨域限制。

Vite 配置代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://backend-server.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
// 前端请求 /api/users → 转发到 http://backend-server.com/users
}
}
}
}

webpack/CRA 配置代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/setupProxy.js(Create React App)
const { createProxyMiddleware } = require('http-proxy-middleware')

module.exports = function (app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://backend-server.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
})
)
}

这个只在开发环境有效,代理服务器和前端同域。生产环境需要用 Nginx 做反向代理。

生产环境:Nginx 反向代理

生产环境最常见的方案是前端和后端都挂在 Nginx 下,通过 Nginx 转发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 80;
server_name your-domain.com;

# 前端静态文件
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}

# API 请求转发到后端
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

这样前端访问 your-domain.com/api/users,Nginx 转发到后端,浏览器看到的始终是同一个域,没有跨域。

JSONP:了解就好,别用了

JSONP 是老方案,原理是利用 <script> 标签不受同源限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function jsonp(url, callback) {
const fnName = 'jsonp_' + Date.now()

window[fnName] = function (data) {
callback(data)
delete window[fnName]
document.body.removeChild(script)
}

const script = document.createElement('script')
script.src = `${url}?callback=${fnName}`
document.body.appendChild(script)
}

jsonp('http://other-domain.com/api/data', (data) => {
console.log(data)
})

服务端返回的不是 JSON,而是一个函数调用:jsonp_1234567890({...})

缺点很明显:只能 GET 请求,有 XSS 风险,已经很少用了。面试提一下原理就行。

postMessage:跨窗口通信

如果是两个不同源的页面(比如主页面和 iframe)需要通信,用 postMessage

1
2
3
4
5
6
7
8
9
10
11
// 发送方
const iframe = document.querySelector('iframe')
iframe.contentWindow.postMessage({ type: 'hello', data: 'world' }, 'https://other-domain.com')

// 接收方(在 iframe 里)
window.addEventListener('message', (event) => {
// 一定要验证来源,防止恶意信息
if (event.origin !== 'https://your-main-domain.com') return

console.log(event.data) // { type: 'hello', data: 'world' }
})

总结一下选哪种方案

场景 推荐方案
能改服务端 配置 CORS 响应头
开发环境调后端接口 本地 devServer 代理
生产环境前后端分离部署 Nginx 反向代理
调第三方不能修改的接口 自建中间层服务做代理
两个页面/iframe 通信 postMessage

遇到跨域问题别慌,先判断是开发环境还是生产环境,再看能不能改服务端,然后按上面的方案选一个。