一、什么是跨域(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:8080 | http://localhost:8080 | ❌ 否 |
| 前后端分离 | http://localhost:3000(React) | http://localhost:8080(Spring Boot) | ✅ 是 |
| 前后端分离 | http://localhost:5173(Vue) | http://localhost:8080(Spring Boot) | ✅ 是 |
2. 生产环境跨域
| 场景 | 前端地址 | 后端地址 | 是否跨域 |
|---|---|---|---|
| 部署在同一服务器 | https://example.com | https://example.com/api | ❌ 否 |
| 部署在不同域名 | https://app.example.com | https://api.example.com | ✅ 是 |
| 使用 CDN 加载前端 | https://cdn.example.com | https://api.example.com | ✅ 是 |
三、CORS 工作原理
1. 简单请求(Simple Request)
触发条件:
- HTTP 方法:GET、POST、HEAD
- 请求头:
- Accept
- Accept-Language
- Content-Language
- Content-Type:
application/x-www-form-urlencoded、multipart/form-data、text/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 反向代理 |