跨域问题解决方案指南

什么是跨域(CORS)?

跨域资源共享(CORS,Cross-Origin Resource Sharing)是浏览器的一种安全机制。当前端应用尝试访问不同域名、端口或协议的 API 时,浏览器会阻止这种请求。

跨域场景示例:

前端:http://localhost:5173
后端:https://juleon.site/api
结果:❌ 跨域错误

解决方案

方案一:开发环境使用 Vite 代理(推荐)

在开发环境中,使用 Vite 的代理功能将请求转发到后端服务器。

1. 配置 vite.config.ts

import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    plugins: [vue()],
    server: {
      port: 5173,
      // 配置代理
      proxy: {
        // 方式一:简单代理
        '/api': {
          target: env.VITE_API_BASE_URL || 'https://juleon.site',
          changeOrigin: true,
          // rewrite: (path) => path.replace(/^\/api/, '') // 如果需要去掉 /api 前缀
        },
        
        // 方式二:多个代理
        '/api/v1': {
          target: 'https://api1.example.com',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api\/v1/, '')
        },
        '/api/v2': {
          target: 'https://api2.example.com',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api\/v2/, '')
        }
      }
    },
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url))
      }
    }
  }
})

2. 配置 axios baseURL

// src/api/request.ts
import axios from 'axios'

const request = axios.create({
  // 开发环境使用相对路径,会被代理转发
  // 生产环境使用完整 URL
  baseURL: import.meta.env.DEV ? '/api' : import.meta.env.VITE_API_BASE_URL,
  timeout: 60000,
  headers: {
    'Content-Type': 'application/json'
  }
})

export default request

3. 环境变量配置

# .env.development
VITE_API_BASE_URL=https://juleon.site/api

# .env.production
VITE_API_BASE_URL=https://juleon.site/api

4. 请求示例

// 前端代码
import request from '@/api/request'

// 实际请求:http://localhost:5173/api/users
// 代理转发到:https://juleon.site/api/users
request.get('/users')

方案二:后端配置 CORS(生产环境推荐)

如果你有后端控制权,可以在后端配置 CORS 响应头。

Node.js (Express) 示例

const express = require('express')
const cors = require('cors')
const app = express()

// 方式一:允许所有来源(不推荐用于生产环境)
app.use(cors())

// 方式二:指定允许的来源(推荐)
app.use(cors({
  origin: [
    'http://localhost:5173',
    'https://yourdomain.com'
  ],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization']
}))

// 或者手动设置响应头
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:5173')
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  res.header('Access-Control-Allow-Credentials', 'true')
  
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200)
  }
  next()
})

Spring Boot 示例

@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                    .allowedOrigins("http://localhost:5173", "https://yourdomain.com")
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                    .allowedHeaders("*")
                    .allowCredentials(true);
            }
        };
    }
}

Nginx 配置

server {
    listen 80;
    server_name api.example.com;

    location /api {
        # 添加 CORS 响应头
        add_header 'Access-Control-Allow-Origin' 'http://localhost:5173' 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;

        # 处理 OPTIONS 预检请求
        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

方案三:使用 JSONP(仅支持 GET 请求,不推荐)

JSONP 是一种老旧的跨域解决方案,现在很少使用。

项目配置示例

当前项目配置

vite.config.ts

import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    plugins: [vue(), vueDevTools()],
    server: {
      proxy: {
        '/api': {
          target: env.VITE_API_BASE_URL || 'http://localhost:3000',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, '/api/')
        }
      }
    },
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url))
      }
    }
  }
})

src/api/request.ts

import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'
import { ElMessage } from 'element-plus'

const request = axios.create({
  // 开发环境使用代理,生产环境使用完整 URL
  baseURL: import.meta.env.DEV ? '/api' : import.meta.env.VITE_API_BASE_URL,
  timeout: 60000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// Request interceptor
request.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error: AxiosError) => {
    return Promise.reject(error)
  }
)

// Response interceptor
request.interceptors.response.use(
  (response) => {
    return response.data
  },
  (error: AxiosError) => {
    const message = (error.response?.data as { message?: string })?.message || '请求失败'
    ElMessage.error(message)
    return Promise.reject(error)
  }
)

export default request

代理配置详解

changeOrigin

proxy: {
  '/api': {
    target: 'https://juleon.site',
    changeOrigin: true  // 修改请求头中的 origin
  }
}
  • true: 将请求头的 origin 改为目标 URL
  • false: 保持原始 origin

rewrite

proxy: {
  '/api': {
    target: 'https://juleon.site',
    rewrite: (path) => path.replace(/^\/api/, '')
  }
}

示例:

  • 前端请求:/api/users
  • 不使用 rewrite:转发到 https://juleon.site/api/users
  • 使用 rewrite:转发到 https://juleon.site/users

secure

proxy: {
  '/api': {
    target: 'https://juleon.site',
    secure: false  // 忽略 SSL 证书验证(仅开发环境)
  }
}

configure

proxy: {
  '/api': {
    target: 'https://juleon.site',
    configure: (proxy, options) => {
      // 自定义代理行为
      proxy.on('proxyReq', (proxyReq, req, res) => {
        console.log('发送请求:', req.method, req.url)
      })
      proxy.on('proxyRes', (proxyRes, req, res) => {
        console.log('收到响应:', proxyRes.statusCode)
      })
    }
  }
}

常见问题

Q1: 代理配置后仍然跨域?

A: 检查以下几点:

  1. 确保重启了开发服务器
  2. 检查 baseURL 配置是否正确
  3. 检查浏览器控制台的实际请求 URL

Q2: 生产环境如何处理跨域?

A: 生产环境有两种方案:

  1. 后端配置 CORS 响应头(推荐)
  2. 使用 Nginx 反向代理

Q3: 如何调试代理?

A: 在 vite.config.ts 中添加日志:

proxy: {
  '/api': {
    target: 'https://juleon.site',
    changeOrigin: true,
    configure: (proxy) => {
      proxy.on('proxyReq', (proxyReq, req) => {
        console.log('代理请求:', req.method, req.url)
      })
      proxy.on('error', (err) => {
        console.log('代理错误:', err)
      })
    }
  }
}

Q4: 如何处理 WebSocket 跨域?

A: 配置 WebSocket 代理:

proxy: {
  '/ws': {
    target: 'ws://localhost:3000',
    ws: true,
    changeOrigin: true
  }
}

Q5: 多个后端服务如何配置?

A: 配置多个代理规则:

proxy: {
  '/api/user': {
    target: 'https://user-service.com',
    changeOrigin: true
  },
  '/api/order': {
    target: 'https://order-service.com',
    changeOrigin: true
  }
}

最佳实践

✅ 推荐做法

  1. 开发环境使用代理:避免在开发时配置后端 CORS
  2. 生产环境配置 CORS:在后端或 Nginx 配置 CORS
  3. 使用环境变量:不同环境使用不同的 API 地址
  4. 统一请求封装:使用 axios 拦截器统一处理
  5. 错误处理:统一处理跨域错误

❌ 避免做法

  1. 不要在生产环境使用 Access-Control-Allow-Origin: *
  2. 不要禁用浏览器的安全策略
  3. 不要在前端代码中硬编码 API 地址
  4. 不要忽略 CORS 预检请求(OPTIONS)

完整示例

开发环境配置

// vite.config.ts
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')
  
  return {
    server: {
      proxy: {
        '/api': {
          target: env.VITE_API_BASE_URL,
          changeOrigin: true,
          configure: (proxy) => {
            proxy.on('error', (err) => {
              console.log('代理错误:', err)
            })
          }
        }
      }
    }
  }
})
// src/api/request.ts
const request = axios.create({
  baseURL: import.meta.env.DEV ? '/api' : import.meta.env.VITE_API_BASE_URL,
  timeout: 60000
})
# .env.development
VITE_API_BASE_URL=https://juleon.site/api

生产环境 Nginx 配置

server {
    listen 80;
    server_name yourdomain.com;

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

    # API 代理
    location /api {
        add_header 'Access-Control-Allow-Origin' 'https://yourdomain.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;

        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass https://juleon.site;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

参考资源