前端代理与跨域实战指南
🎯 从开发环境到生产部署,彻底搞懂代理配置
1. 跨域问题的本质
1.1 同源策略 (Same-Origin Policy)
┌─────────────────────────────────────────────────────────────────────────┐
│ 同源策略判断 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ URL 组成: 协议://主机名:端口/路径 │
│ https://example.com:443/api/users │
│ │
│ 同源判断 (三者必须完全相同): │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ 当前页面 │ 目标 URL │ 是否同源 │ │
│ ├─────────────────────────────┼───────────────────────┼────────────┤ │
│ │ https://app.com/page │ https://app.com/api │ ✅ 同源 │ │
│ │ https://app.com/page │ http://app.com/api │ ❌ 协议不同 │ │
│ │ https://app.com/page │ https://api.app.com │ ❌ 主机不同 │ │
│ │ https://app.com/page │ https://app.com:8080 │ ❌ 端口不同 │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ 跨域限制范围: │
│ • AJAX 请求 (XMLHttpRequest / Fetch) │
│ • Cookie、LocalStorage、IndexedDB │
│ • DOM 操作 (iframe) │
│ │
│ 不受限制: │
│ • <script src="..."> │
│ • <link href="..."> │
│ • <img src="..."> │
│ • <video> / <audio> │
│ │
└─────────────────────────────────────────────────────────────────────────┘1.2 为什么需要同源策略?
javascript
// 没有同源策略的危险场景:
// 1. 用户登录银行网站 bank.com,Cookie 保存了身份凭证
// 2. 用户访问恶意网站 evil.com
// 3. evil.com 的 JS 代码:
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include', // 携带 Cookie
body: JSON.stringify({
to: 'hacker_account',
amount: 100000
})
});
// 如果没有同源策略,这个请求会带上 bank.com 的 Cookie
// 银行服务器认为是合法用户操作,转账成功!2. 开发环境代理
2.1 代理工作原理
┌─────────────────────────────────────────────────────────────────────────┐
│ 开发环境代理原理 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 为什么需要开发代理? │
│ ───────────────── │
│ 前端: http://localhost:5173 │
│ 后端: http://localhost:3000/api │
│ → 端口不同,跨域!浏览器拦截请求 │
│ │
│ 无代理时 (跨域失败): │
│ ┌─────────┐ /api/users ┌─────────┐ │
│ │ Browser │ ────────────▶ │ Backend │ │
│ │ :5173 │ ❌ │ :3000 │ │
│ └─────────┘ CORS Error └─────────┘ │
│ │
│ 有代理时 (同源请求): │
│ ┌─────────┐ /api/users ┌─────────┐ 转发请求 ┌─────────┐ │
│ │ Browser │ ────────────▶ │ Vite │ ──────────▶ │ Backend │ │
│ │ │ ✅ │ DevSrv │ │ :3000 │ │
│ └─────────┘ │ :5173 │ ◀────────── └─────────┘ │
│ ▲ └─────────┘ 返回数据 │
│ │ │ │
│ └─────────────────────────┘ │
│ 转发响应 │
│ │
│ 关键: 浏览器看到的是 localhost:5173 → localhost:5173,同源! │
│ 代理服务器在服务端转发,不受同源策略限制 │
│ │
└─────────────────────────────────────────────────────────────────────────┘2.2 Vite 代理配置
typescript
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 5173,
proxy: {
// 基础配置: /api 开头的请求转发到后端
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
// 路径重写: /api/users → /users
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
// 多后端服务
'/user-service': {
target: 'http://localhost:3001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/user-service/, ''),
},
'/order-service': {
target: 'http://localhost:3002',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/order-service/, ''),
},
// WebSocket 代理
'/socket.io': {
target: 'ws://localhost:3000',
ws: true,
},
// HTTPS 后端 (忽略证书校验)
'/secure-api': {
target: 'https://dev-api.example.com',
changeOrigin: true,
secure: false, // 忽略自签名证书
},
},
},
});2.3 Webpack (Create React App) 代理配置
javascript
// src/setupProxy.js (CRA 自动加载)
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
// 基础代理
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:3000',
changeOrigin: true,
})
);
// 带路径重写
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': '', // /api/users → /users
},
})
);
// 多个路径
app.use(
['/api', '/auth'],
createProxyMiddleware({
target: 'http://localhost:3000',
changeOrigin: true,
})
);
// 条件代理
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:3000',
changeOrigin: true,
router: {
// 特定路径转发到不同服务
'/api/users': 'http://localhost:3001',
'/api/orders': 'http://localhost:3002',
},
})
);
};2.4 完整的开发环境配置示例
typescript
// vite.config.ts - 真实项目配置
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: env.VITE_API_BASE_URL || 'http://localhost:3000',
changeOrigin: true,
configure: (proxy, options) => {
// 请求日志
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log(`[Proxy] ${req.method} ${req.url} → ${options.target}${req.url}`);
});
// 响应日志
proxy.on('proxyRes', (proxyRes, req, res) => {
console.log(`[Proxy] ${proxyRes.statusCode} ${req.url}`);
});
// 错误处理
proxy.on('error', (err, req, res) => {
console.error(`[Proxy Error] ${err.message}`);
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Proxy Error', message: err.message }));
});
},
},
},
},
};
});# .env.development
VITE_API_BASE_URL=http://localhost:3000
# .env.staging
VITE_API_BASE_URL=https://staging-api.example.com
# .env.production
VITE_API_BASE_URL=https://api.example.com3. 生产环境代理 (Nginx)
3.1 基础反向代理配置
nginx
# /etc/nginx/conf.d/app.conf
upstream backend {
server 127.0.0.1:3000 weight=5;
server 127.0.0.1:3001 weight=3;
server 127.0.0.1:3002 weight=2;
keepalive 32; # 保持连接池
}
server {
listen 80;
server_name app.example.com;
# 前端静态资源
location / {
root /var/www/app/dist;
index index.html;
try_files $uri $uri/ /index.html; # SPA 路由支持
}
# API 反向代理
location /api/ {
proxy_pass http://backend/;
# 传递客户端真实信息
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;
# 超时设置
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# 缓冲设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# WebSocket 代理
location /ws/ {
proxy_pass http://backend/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400; # 长连接超时
}
}3.2 开发与生产代理对比
┌─────────────────────────────────────────────────────────────────────────┐
│ 开发代理 vs 生产代理 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 维度 开发环境 (Vite) 生产环境 (Nginx) │
│ ──── ───────────────── ───────────────── │
│ │
│ 目的 绕过跨域限制 反向代理、负载均衡 │
│ │
│ 运行位置 本地 Node.js 进程 生产服务器 │
│ │
│ 性能要求 低 (开发使用) 高 (承载用户流量) │
│ │
│ 配置复杂度 简单 复杂 (需考虑缓存、限流等) │
│ │
│ 负载均衡 不需要 需要 (upstream) │
│ │
│ SSL 终止 通常不需要 需要 (HTTPS) │
│ │
│ 典型工具 Vite DevServer Nginx、Kong、Traefik │
│ http-proxy-middleware │
│ │
└─────────────────────────────────────────────────────────────────────────┘3.3 Nginx 高级配置
nginx
# 完整生产配置示例
# 限流配置
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
listen 443 ssl http2;
server_name app.example.com;
# SSL 配置
ssl_certificate /etc/nginx/ssl/app.crt;
ssl_certificate_key /etc/nginx/ssl/app.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# 前端资源 - 长缓存
location /assets/ {
root /var/www/app/dist;
expires 1y;
add_header Cache-Control "public, immutable";
}
# API 代理 - 带限流
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 10;
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# CORS 头 (如果后端没处理)
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Allow-Credentials "true" always;
# 预检请求直接返回
if ($request_method = 'OPTIONS') {
add_header Access-Control-Max-Age 86400;
return 204;
}
}
# 流式响应 (LLM)
location /api/chat/stream {
proxy_pass http://backend/;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 关闭缓冲,确保实时传输
proxy_buffering off;
proxy_cache off;
# SSE 特殊处理
proxy_set_header Accept "text/event-stream";
chunked_transfer_encoding on;
}
# SPA 路由兜底
location / {
root /var/www/app/dist;
try_files $uri $uri/ /index.html;
}
}
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}4. CORS (跨域资源共享)
4.1 CORS 工作流程
┌─────────────────────────────────────────────────────────────────────────┐
│ CORS 完整流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 简单请求 (不触发预检): │
│ ─────────────────── │
│ 条件: GET/HEAD/POST + 简单 Headers + 简单 Content-Type │
│ │
│ ┌────────┐ ┌────────┐ │
│ │ Client │ ─────── GET /api/data ──────▶ │ Server │ │
│ │ │ ◀─────── 200 OK ───────────── │ │ │
│ └────────┘ + Access-Control-Allow-* └────────┘ │
│ │
│ 复杂请求 (触发预检): │
│ ─────────────────── │
│ 触发条件: │
│ • PUT/DELETE/PATCH 等方法 │
│ • 自定义 Header (如 Authorization) │
│ • Content-Type: application/json │
│ │
│ ┌────────┐ ┌────────┐ │
│ │ Client │ ──── OPTIONS /api/data ─────▶ │ Server │ ① 预检请求 │
│ │ │ ◀───── 204 No Content ─────── │ │ ② 预检响应 │
│ │ │ + Access-Control-Allow-* │ │ │
│ │ │ │ │ │
│ │ │ ────── POST /api/data ──────▶ │ │ ③ 实际请求 │
│ │ │ ◀────── 200 OK ───────────── │ │ ④ 实际响应 │
│ └────────┘ + Access-Control-Allow-* └────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘4.2 CORS 响应头详解
javascript
// 预检请求 (OPTIONS) 响应头
{
// 允许的源 (必需)
'Access-Control-Allow-Origin': 'https://app.example.com',
// 或者 '*' (但不能与 credentials 同时使用)
// 允许的方法
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
// 允许的请求头
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Request-Id',
// 是否允许携带 Cookie
'Access-Control-Allow-Credentials': 'true',
// 预检缓存时间 (秒)
'Access-Control-Max-Age': '86400',
}
// 实际请求响应头
{
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Credentials': 'true',
// 允许前端访问的响应头
'Access-Control-Expose-Headers': 'X-Total-Count, X-Request-Id',
}4.3 后端 CORS 配置
javascript
// Express.js
const cors = require('cors');
// 简单配置
app.use(cors());
// 完整配置
app.use(cors({
origin: (origin, callback) => {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
];
// 允许无 origin (如 Postman、curl)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
exposedHeaders: ['X-Total-Count'],
credentials: true,
maxAge: 86400,
}));typescript
// NestJS
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: ['https://app.example.com'],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
});
await app.listen(3000);
}4.4 Cookie 跨域携带
javascript
// 前端: 需要设置 credentials
fetch('https://api.example.com/user', {
method: 'GET',
credentials: 'include', // 携带 Cookie
});
// axios
axios.get('https://api.example.com/user', {
withCredentials: true,
});
// 后端响应头必须满足:
// 1. Access-Control-Allow-Credentials: true
// 2. Access-Control-Allow-Origin 不能是 *,必须是具体域名
// 3. Cookie 必须设置 SameSite=None; Secure (跨站场景)5. 常见代理问题排查
5.1 代理调试清单
┌─────────────────────────────────────────────────────────────────────────┐
│ 代理问题排查清单 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 问题: 代理不生效,仍然跨域 │
│ ───────────────────────────── │
│ □ 检查请求路径是否匹配代理规则 │
│ □ 检查是否使用了完整 URL (应该用相对路径) │
│ □ 检查 DevServer 是否正确启动 │
│ │
│ // ❌ 错误: 使用完整 URL,不走代理 │
│ fetch('http://localhost:3000/api/users') │
│ │
│ // ✅ 正确: 使用相对路径,走代理 │
│ fetch('/api/users') │
│ │
│ ❌ 问题: 代理返回 502 Bad Gateway │
│ ───────────────────────────────── │
│ □ 检查后端服务是否启动 │
│ □ 检查 target 地址是否正确 │
│ □ 检查网络连通性 (防火墙、Docker 网络) │
│ │
│ ❌ 问题: WebSocket 代理失败 │
│ ──────────────────────────── │
│ □ 确保配置了 ws: true │
│ □ 检查 target 是否使用 ws:// 协议 │
│ □ Nginx 需要配置 Upgrade 头 │
│ │
│ ❌ 问题: CORS 预检请求失败 │
│ ───────────────────────────── │
│ □ 后端需要处理 OPTIONS 请求 │
│ □ 检查 Access-Control-Allow-* 响应头 │
│ □ credentials 和 * origin 不能同时使用 │
│ │
└─────────────────────────────────────────────────────────────────────────┘5.2 代理日志调试
typescript
// vite.config.ts - 详细日志
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq, req) => {
console.log('─'.repeat(50));
console.log(`[→ Request] ${req.method} ${req.url}`);
console.log(`[→ Target] ${proxyReq.protocol}//${proxyReq.host}${proxyReq.path}`);
console.log(`[→ Headers]`, JSON.stringify(proxyReq.getHeaders(), null, 2));
});
proxy.on('proxyRes', (proxyRes, req) => {
console.log(`[← Response] ${proxyRes.statusCode} ${req.url}`);
console.log(`[← Headers]`, JSON.stringify(proxyRes.headers, null, 2));
});
proxy.on('error', (err, req) => {
console.error(`[✗ Error] ${req.url}`, err.message);
});
},
},
},
},
});6. 真实案例
案例 1: 微服务架构下的代理配置
typescript
// vite.config.ts - 多服务代理
const serviceMap = {
'/api/user': 'http://user-service:3001',
'/api/order': 'http://order-service:3002',
'/api/product': 'http://product-service:3003',
'/api/payment': 'http://payment-service:3004',
};
export default defineConfig({
server: {
proxy: Object.fromEntries(
Object.entries(serviceMap).map(([path, target]) => [
path,
{
target,
changeOrigin: true,
rewrite: (p) => p.replace(new RegExp(`^${path}`), ''),
},
])
),
},
});nginx
# nginx.conf - 生产环境微服务路由
upstream user-service {
server user-service:3001;
}
upstream order-service {
server order-service:3002;
}
server {
location /api/user/ {
proxy_pass http://user-service/;
}
location /api/order/ {
proxy_pass http://order-service/;
}
}案例 2: 本地 Mock + 远程 API 混合代理
typescript
// vite.config.ts
export default defineConfig({
server: {
proxy: {
// 本地 Mock 的接口
'/api/mock': {
target: 'http://localhost:4000', // Mock Server
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/mock/, ''),
},
// 连接远程测试环境
'/api': {
target: 'https://test-api.example.com',
changeOrigin: true,
secure: false,
},
},
},
});案例 3: 处理 Cookie Domain 问题
typescript
// 问题: 后端设置的 Cookie domain 是 api.example.com
// 前端 localhost 无法使用该 Cookie
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
cookieDomainRewrite: {
// 把 Cookie 的 domain 重写为 localhost
'api.example.com': 'localhost',
},
cookiePathRewrite: {
// 把 Cookie 的 path 重写
'/api': '/',
},
},
},
},
});7. 面试高频问题
Q1: 开发环境代理和生产环境代理的区别?
| 维度 | 开发代理 | 生产代理 |
|---|---|---|
| 目的 | 绕过同源策略 | 负载均衡、反向代理 |
| 原理 | Node.js http-proxy | Nginx upstream |
| 性能 | 低 | 高 (C 实现) |
| 功能 | 简单转发 | 缓存、限流、SSL终止 |
Q2: 为什么开发代理能绕过跨域?
浏览器的同源策略只限制浏览器到服务器的请求。开发代理将请求转发变成服务器到服务器的请求,不受同源策略限制。
浏览器 → Vite DevServer (同源) → 后端 API (服务端请求,无跨域限制)Q3: CORS 预检请求在什么情况下触发?
触发条件 (满足任一):
- HTTP 方法不是 GET/HEAD/POST
- Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data、text/plain
- 包含自定义 Header (如 Authorization)
- 使用了 ReadableStream
Q4: 如何让 Cookie 跨域携带?
- 前端:
credentials: 'include'或withCredentials: true - 后端:
Access-Control-Allow-Credentials: true - 后端:
Access-Control-Allow-Origin必须是具体域名,不能是* - Cookie:
SameSite=None; Secure(跨站场景)
Q5: Nginx 反向代理需要配置哪些关键头部?
nginx
proxy_set_header Host $host; # 原始主机名
proxy_set_header X-Real-IP $remote_addr; # 客户端真实 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链
proxy_set_header X-Forwarded-Proto $scheme; # 原始协议 (http/https)