6. 前后端联调与跨域(CORS)

3 阅读6分钟

一、什么是跨域(CORS)?

1. 跨域的定义

同源策略(Same-Origin Policy):浏览器的安全机制,限制一个源(Origin)的文档或脚本如何与另一个源的资源进行交互。

源(Origin) = 协议 + 域名 + 端口

URL说明
http://localhost:8080源:http://localhost:8080
http://localhost:3000源:http://localhost:3000不同源
https://example.com:8080源:https://example.com:8080(协议不同)
http://example.com:9090源:http://example.com:9090(端口不同)

2. 跨域场景

同源(允许访问):

// 前端:http://localhost:8080
// 后端:http://localhost:8080/api/users
// ✅ 同源,允许访问
fetch('http://localhost:8080/api/users')

跨域(被浏览器阻止):

// 前端:http://localhost:3000(React/Vue 开发服务器)
// 后端:http://localhost:8080(Spring Boot)
// ❌ 跨域,浏览器阻止访问
fetch('http://localhost:8080/api/users')

3. 跨域错误示例

浏览器控制台错误:

Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:3000' 
has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

二、为什么会有跨域问题?

1. 前后端分离开发

开发方式前端地址后端地址是否跨域
传统开发http://localhost:8080http://localhost:8080❌ 否
前后端分离http://localhost:3000(React)http://localhost:8080(Spring Boot)✅ 是
前后端分离http://localhost:5173(Vue)http://localhost:8080(Spring Boot)✅ 是

2. 生产环境跨域

场景前端地址后端地址是否跨域
部署在同一服务器https://example.comhttps://example.com/api❌ 否
部署在不同域名https://app.example.comhttps://api.example.com✅ 是
使用 CDN 加载前端https://cdn.example.comhttps://api.example.com✅ 是

三、CORS 工作原理

1. 简单请求(Simple Request)

触发条件:

  • HTTP 方法:GET、POST、HEAD
  • 请求头:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type:application/x-www-form-urlencodedmultipart/form-datatext/plain

流程:

前端请求 → 后端处理 → 响应包含 Access-Control-Allow-Origin
         ↓
      浏览器检查 Access-Control-Allow-Origin 是否包含前端源
         ↓
    包含:允许访问
    不包含:阻止访问

2. 预检请求(Preflight Request)

触发条件:

  • HTTP 方法:PUT、DELETE、PATCH
  • 请求头:Content-Type: application/json
  • 自定义请求头(如 Authorization

流程:

1. OPTIONS 预检请求
   前端 → 后端:OPTIONS /api/users
   请求头:
   - Origin: http://localhost:3000
   - Access-Control-Request-Method: POST
   - Access-Control-Request-Headers: content-type

2. 预检响应
   后端 → 前端:HTTP 204 No Content
   响应头:
   - Access-Control-Allow-Origin: http://localhost:3000
   - Access-Control-Allow-Methods: POST, GET, OPTIONS
   - Access-Control-Allow-Headers: content-type
   - Access-Control-Max-Age: 3600(缓存预检结果)

3. 实际请求
   前端 → 后端:POST /api/users
   请求头:
   - Origin: http://localhost:3000
   - Content-Type: application/json

4. 实际响应
   后端 → 前端:HTTP 200 OK
   响应头:
   - Access-Control-Allow-Origin: http://localhost:3000
   - Content-Type: application/json

四、Spring Boot 解决跨域问题

1. 方法一:@CrossOrigin 注解

使用方式:

@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserController {
    
    @GetMapping
    @CrossOrigin(origins = {"http://localhost:3000", "http://localhost:5173"})
    public ResponseEntity<List<UserDTO>> findAll() {
        // ...
    }
    
    @PostMapping
    public ResponseEntity<UserDTO> create(@Valid @RequestBody CreateUserDTO userDTO) {
        // ...
    }
}

优缺点:

优点缺点
简单直接每个控制器都需要添加
灵活控制每个接口代码重复
易于理解难以统一管理

2. 方法二:全局 CORS 配置(推荐)

配置类:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")  // 允许跨域的路径
                .allowedOrigins(         // 允许的前端源
                        "http://localhost:3000",      // React
                        "http://localhost:5173",     // Vue
                        "https://example.com"        // 生产环境
                )
                .allowedMethods(          // 允许的 HTTP 方法
                        "GET", "POST", "PUT", "DELETE", "OPTIONS"
                )
                .allowedHeaders("*")      // 允许的请求头
                .allowCredentials(true)    // 允许发送 Cookie
                .maxAge(3600);            // 预检请求缓存时间(秒)
    }
}

优缺点:

优点缺点
统一管理 CORS 配置需要创建配置类
避免代码重复灵活性略低
适合中大型项目

3. 方法三:基于注解的配置

添加注解:

@RestController
@RequestMapping("/api")
@CrossOrigin(
    origins = {"http://localhost:3000", "http://localhost:5173"},
    methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE},
    allowedHeaders = "*",
    allowCredentials = "true",
    maxAge = 3600
)
public class ApiController {
    // ...
}

五、前端调用后端 API(完整示例)

1. React 示例

UserService.js:

import axios from 'axios';

const API_BASE_URL = 'http://localhost:8080/api';

// 获取所有用户
export const getAllUsers = async () => {
    try {
        const response = await axios.get(`${API_BASE_URL}/users`);
        return response.data;
    } catch (error) {
        console.error('获取用户列表失败:', error);
        throw error;
    }
};

// 创建用户
export const createUser = async (userData) => {
    try {
        const response = await axios.post(
            `${API_BASE_URL}/users`,
            userData,
            {
                headers: {
                    'Content-Type': 'application/json',
                },
            }
        );
        return response.data;
    } catch (error) {
        console.error('创建用户失败:', error);
        throw error;
    }
};

// 更新用户
export const updateUser = async (id, userData) => {
    try {
        const response = await axios.put(
            `${API_BASE_URL}/users/${id}`,
            userData
        );
        return response.data;
    } catch (error) {
        console.error('更新用户失败:', error);
        throw error;
    }
};

// 删除用户
export const deleteUser = async (id) => {
    try {
        await axios.delete(`${API_BASE_URL}/users/${id}`);
    } catch (error) {
        console.error('删除用户失败:', error);
        throw error;
    }
};

UserList.js:

import { useState, useEffect } from 'react';
import { getAllUsers, deleteUser } from './UserService';

function UserList() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    // 获取用户列表
    const fetchUsers = async () => {
        try {
            setLoading(true);
            const data = await getAllUsers();
            setUsers(data);
            setError(null);
        } catch (err) {
            setError('获取用户列表失败');
            console.error(err);
        } finally {
            setLoading(false);
        }
    };

    // 删除用户
    const handleDelete = async (id) => {
        if (window.confirm('确定要删除该用户吗?')) {
            try {
                await deleteUser(id);
                // 重新获取用户列表
                fetchUsers();
                alert('删除成功!');
            } catch (err) {
                alert('删除失败!');
                console.error(err);
            }
        }
    };

    // 组件挂载时获取数据
    useEffect(() => {
        fetchUsers();
    }, []);

    if (loading) return <div>加载中...</div>;
    if (error) return <div>{error}</div>;

    return (
        <div>
            <h2>用户列表</h2>
            <table border="1">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>姓名</th>
                        <th>邮箱</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    {users.map(user => (
                        <tr key={user.id}>
                            <td>{user.id}</td>
                            <td>{user.name}</td>
                            <td>{user.email}</td>
                            <td>
                                <button onClick={() => handleDelete(user.id)}>
                                    删除
                                </button>
                            </td>
                        </tr>
                    ))}
                </tbody>
            </table>
        </div>
    );
}

export default UserList;

2. Vue 3 示例

userService.js:

import axios from 'axios';

const API_BASE_URL = 'http://localhost:8080/api';

export default {
    // 获取所有用户
    async getAllUsers() {
        try {
            const response = await axios.get(`${API_BASE_URL}/users`);
            return response.data;
        } catch (error) {
            console.error('获取用户列表失败:', error);
            throw error;
        }
    },

    // 创建用户
    async createUser(userData) {
        try {
            const response = await axios.post(
                `${API_BASE_URL}/users`,
                userData
            );
            return response.data;
        } catch (error) {
            console.error('创建用户失败:', error);
            throw error;
        }
    },

    // 更新用户
    async updateUser(id, userData) {
        try {
            const response = await axios.put(
                `${API_BASE_URL}/users/${id}`,
                userData
            );
            return response.data;
        } catch (error) {
            console.error('更新用户失败:', error);
            throw error;
        }
    },

    // 删除用户
    async deleteUser(id) {
        try {
            await axios.delete(`${API_BASE_URL}/users/${id}`);
        } catch (error) {
            console.error('删除用户失败:', error);
            throw error;
        }
    }
};

UserList.vue:

<template>
    <div>
        <h2>用户列表</h2>
        
        <div v-if="loading">加载中...</div>
        <div v-if="error" class="error">{{ error }}</div>
        
        <table v-if="!loading && !error" border="1">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>姓名</th>
                    <th>邮箱</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <tr v-for="user in users" :key="user.id">
                    <td>{{ user.id }}</td>
                    <td>{{ user.name }}</td>
                    <td>{{ user.email }}</td>
                    <td>
                        <button @click="handleDelete(user.id)">
                            删除
                        </button>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</template>

<script>
import userService from './userService';

export default {
    name: 'UserList',
    data() {
        return {
            users: [],
            loading: true,
            error: null
        };
    },
    mounted() {
        this.fetchUsers();
    },
    methods: {
        async fetchUsers() {
            try {
                this.loading = true;
                this.users = await userService.getAllUsers();
                this.error = null;
            } catch (err) {
                this.error = '获取用户列表失败';
                console.error(err);
            } finally {
                this.loading = false;
            }
        },
        
        async handleDelete(id) {
            if (confirm('确定要删除该用户吗?')) {
                try {
                    await userService.deleteUser(id);
                    this.fetchUsers();
                    alert('删除成功!');
                } catch (err) {
                    alert('删除失败!');
                    console.error(err);
                }
            }
        }
    }
};
</script>

<style scoped>
.error {
    color: red;
}
</style>

六、环境配置最佳实践

1. 区分开发环境和生产环境

application-dev.yml(开发环境):

server:
  port: 8080

cors:
  allowed-origins:
    - http://localhost:3000
    - http://localhost:5173

application-prod.yml(生产环境):

server:
  port: 8080

cors:
  allowed-origins:
    - https://example.com

CorsConfig.java:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Value("${cors.allowed-origins}")
    private String[] allowedOrigins;
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins(allowedOrigins)
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

2. 使用 Nginx 反向代理(生产环境)

Nginx 配置:

server {
    listen 80;
    server_name example.com;

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

    # 后端 API 代理
    location /api/ {
        proxy_pass http://localhost:8080/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

优点:

  • 前后端同源,不需要 CORS 配置
  • 统一域名,便于管理
  • 支持 HTTPS 配置
  • 负载均衡和缓存

七、常见问题与解决方案

1. 预检请求失败

错误:

Request header field content-type is not allowed by Access-Control-Allow-Headers

解决:

.allowedHeaders("*")  // 允许所有请求头

2. Cookie 传递失败

前端:

axios.get('http://localhost:8080/api/users', {
    withCredentials: true  // 携带 Cookie
});

后端:

.allowCredentials(true)  // 允许携带 Cookie
.allowedOrigins("http://localhost:3000")  // 不能使用 "*"

3. 动态请求头问题

错误:

Request header field authorization is not allowed

解决:

.allowedHeaders("Content-Type", "Authorization")

4. 跨域与认证冲突

问题:

  • 使用 JWT 认证时,前端需要在请求头中携带 Authorization
  • 预检请求可能被阻止

解决:

.allowedHeaders("Authorization", "Content-Type")

八、总结

概念说明
跨域不同源之间的请求,被浏览器阻止
同源协议 + 域名 + 端口相同
CORS跨域资源共享,允许跨域访问
预检请求OPTIONS 请求,检查是否允许实际请求
配置方式@CrossOrigin 注解、全局 CORS 配置、Nginx 反向代理
最佳实践开发环境配置 CORS,生产环境使用 Nginx 反向代理