Skip to content

前端代理与跨域实战指南

🎯 从开发环境到生产部署,彻底搞懂代理配置


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.com

3. 生产环境代理 (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);
}
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,
            },
        },
    },
});
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-proxyNginx upstream
性能高 (C 实现)
功能简单转发缓存、限流、SSL终止

Q2: 为什么开发代理能绕过跨域?

浏览器的同源策略只限制浏览器到服务器的请求。开发代理将请求转发变成服务器到服务器的请求,不受同源策略限制。

浏览器 → Vite DevServer (同源) → 后端 API (服务端请求,无跨域限制)

Q3: CORS 预检请求在什么情况下触发?

触发条件 (满足任一):

  1. HTTP 方法不是 GET/HEAD/POST
  2. Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data、text/plain
  3. 包含自定义 Header (如 Authorization)
  4. 使用了 ReadableStream
  1. 前端: credentials: 'include'withCredentials: true
  2. 后端: Access-Control-Allow-Credentials: true
  3. 后端: Access-Control-Allow-Origin 必须是具体域名,不能是 *
  4. 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)

前端面试知识库