Spring Boot 跨域(CORS)问题
一、什么是跨域问题?
1.1 同源策略
浏览器出于安全考虑,实施了同源策略(Same-Origin Policy),限制网页向不同源的服务器发送请求。
同源的定义:协议 + 域名 + 端口 完全相同
✅ 同源示例:
http://localhost:8080/api/user
http://localhost:8080/api/order
❌ 不同源示例:
http://localhost:5173 → http://localhost:8080 (端口不同)
http://example.com → https://example.com (协议不同)
http://www.a.com → http://www.b.com (域名不同)
1.2 跨域场景
┌─────────────────────┐ ┌─────────────────────┐
│ 前端 Vue 应用 │ HTTP │ 后端 Spring Boot │
│ localhost:5173 │ ──────▶ │ localhost:8080 │
└─────────────────────┘ 跨域! └─────────────────────┘
当前端(如 Vue)和后端(如 Spring Boot)部署在不同端口/域名时,浏览器会阻止请求。
二、CORS 机制解析
2.1 什么是 CORS?
CORS(Cross-Origin Resource Sharing,跨域资源共享)是 W3C 标准,通过服务器设置 HTTP 响应头,告诉浏览器允许哪些跨域请求。
2.2 CORS 响应头说明
| 响应头 | 说明 | 示例值 |
|---|---|---|
Access-Control-Allow-Origin |
允许的来源域名 | http://localhost:5173 或 * |
Access-Control-Allow-Methods |
允许的 HTTP 方法 | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers |
允许的请求头 | Content-Type, Authorization |
Access-Control-Allow-Credentials |
是否允许携带 Cookie | true / false |
Access-Control-Max-Age |
预检请求缓存时间(秒) | 3600 |
2.3 简单请求 vs 预检请求
简单请求(直接发送):
┌────────┐ GET/POST ┌────────┐
│ 浏览器 │ ─────────▶ │ 服务器 │
└────────┘ └────────┘
预检请求(先 OPTIONS 探测):
┌────────┐ OPTIONS ┌────────┐
│ 浏览器 │ ─────────▶ │ 服务器 │ ← 预检请求
│ │ ◀───────── │ │ ← 返回 CORS 头
│ │ PUT/DELETE│ │
│ │ ─────────▶ │ │ ← 实际请求
└────────┘ └────────┘
触发预检请求的条件:
- 使用
PUT、DELETE、PATCH方法 - 自定义请求头(如
Authorization) Content-Type不是application/x-www-form-urlencoded、multipart/form-data、text/plain
三、解决方案演进
方案一:WebMvcConfigurer(基础硬编码)
最简单的方式,直接在代码中写死允许的域名。
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 所有路径
.allowedOrigins( // 允许的域名(硬编码)
"http://localhost:5173",
"http://127.0.0.1:5173"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true) // 允许携带 Cookie
.maxAge(3600); // 预检缓存 1 小时
}
}
优点:简单直接 缺点:修改域名需要改代码并重新部署
方案二:Filter 过滤器(精细控制)
通过过滤器手动设置响应头,适合需要更精细控制的场景。
package com.example.config;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级,确保最先执行
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 动态获取请求来源
String origin = request.getHeader("Origin");
if (origin != null) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
// 设置其他 CORS 头
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
// 动态获取请求头
String headers = request.getHeader("Access-Control-Request-Headers");
if (headers != null) {
response.setHeader("Access-Control-Allow-Headers", headers);
}
// 预检请求直接返回
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
}
优点:可以动态处理、精细控制 缺点:允许所有来源可能有安全风险
方案三:从配置文件读取(推荐)
将允许的域名配置到 application.yml 中,无需改代码即可调整。
3.1 配置文件
# application.yml
cors:
allowed-origins: ${APP_CORS_ORIGINS:http://localhost:5173,http://localhost:3000}
3.2 配置属性类
package com.example.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Data
@Component
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
/**
* 允许的跨域来源列表
*/
private List<String> allowedOrigins;
}
3.3 CORS 配置类
package com.example.config;
import com.example.config.properties.CorsProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class CorsConfig implements WebMvcConfigurer {
private final CorsProperties corsProperties;
@Override
public void addCorsMappings(CorsRegistry registry) {
// 从配置文件读取允许的域名
String[] origins = corsProperties.getAllowedOrigins()
.toArray(new String[0]);
registry.addMapping("/**")
.allowedOrigins(origins) // 动态配置
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
3.4 环境变量文件
# .env.dev - 开发环境
APP_CORS_ORIGINS=http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173
# .env.prod - 生产环境
APP_CORS_ORIGINS=https://www.example.com,https://admin.example.com
优点:
- 不同环境配置不同域名
- 无需改代码,改配置即可
- 敏感信息不提交 Git
四、其他跨域解决方案
4.1 前端代理(开发环境)
在前端开发服务器配置代理,将请求转发到后端。
// vite.config.js (Vue 3 + Vite)
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
// vue.config.js (Vue 2 + Webpack)
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
注意:仅适用于开发环境,生产环境需要后端配置 CORS。
4.2 Nginx 反向代理(生产环境)
server {
listen 80;
server_name www.example.com;
# 前端静态资源
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
# 后端 API 代理
location /api {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
优点:前后端同域,无需配置 CORS
4.3 JSONP(已过时)
通过 `` 标签加载,绕过同源策略。
// 仅支持 GET 请求,有安全风险,不推荐使用
function jsonp(url, callback) {
const script = document.createElement('script');
script.src = `${url}?callback=${callback}`;
document.body.appendChild(script);
}
五、方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| WebMvcConfigurer 硬编码 | 简单项目 | 代码简单 | 改域名要改代码 |
| Filter 过滤器 | 需要精细控制 | 灵活度高 | 代码较多 |
| 配置文件读取 | 生产项目(推荐) | 灵活、安全 | 需要配置类 |
| 前端代理 | 开发环境 | 前端独立配置 | 仅开发环境 |
| Nginx 代理 | 生产环境 | 彻底解决跨域 | 需要运维配置 |
六、常见问题
6.1 allowCredentials 与 allowedOrigins
// ❌ 错误:allowCredentials=true 时不能用 *
.allowedOrigins("*")
.allowCredentials(true)
// ✅ 正确:必须指定具体域名
.allowedOrigins("http://localhost:5173")
.allowCredentials(true)
// ✅ 或者使用 allowedOriginPatterns
.allowedOriginPatterns("*")
.allowCredentials(true)
6.2 预检请求 401/403
如果使用了 Security 或拦截器,需要放行 OPTIONS 请求:
// Spring Security 配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors() // 启用 CORS
.and()
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 放行预检
.anyRequest().authenticated()
);
return http.build();
}
6.3 多个 CORS 配置冲突
确保只有一个 CORS 配置生效:
// ❌ 不要同时使用 @CrossOrigin 和全局配置
@CrossOrigin(origins = "http://localhost:5173")
@RestController
public class UserController { }
// ✅ 统一使用全局配置
七、推荐配置(完整示例)
项目结构
src/main/java/com/example/
├── config/
│ ├── CorsConfig.java # CORS 配置
│ └── properties/
│ └── CorsProperties.java # CORS 属性
└── resources/
├── application.yml # 主配置
├── application-dev.yml # 开发环境
└── application-prod.yml # 生产环境
项目根目录/
├── .env.dev # 开发环境变量
└── .env.prod # 生产环境变量
完整代码
CorsProperties.java
@Data
@Component
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
private List<String> allowedOrigins = new ArrayList<>();
}
CorsConfig.java
@Configuration
@RequiredArgsConstructor
public class CorsConfig implements WebMvcConfigurer {
private final CorsProperties corsProperties;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0]))
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
也可以不需要CorsProperties.java,直接CorsConfig.java
不需要为了"规范"而过度设计。等项目真正需要更多 CORS 配置项时,再重构成 Properties 类也不迟。
CorsConfig.java
package com.zwnsyw.zwwwspringbootbasetemplate.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域资源共享(CORS)配置
* <p>
* 解决前后端分离项目中的跨域访问问题
* </p>
*
* @author your-name
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
/**
* 允许的前端域名列表(从配置文件读取)
* <p>
* 默认值为 "*"(允许所有域名)
* 注意:使用 allowedOriginPatterns 而非 allowedOrigins,
* 因此即使设置 allowCredentials=true 也支持通配符
* </p>
*/
@Value("${cors.allowed-origins:*}")
private String[] allowedOrigins;
/**
* 预检请求缓存时间(秒)
* <p>
* 浏览器会缓存 OPTIONS 预检请求的结果,避免频繁发送预检请求
* </p>
*/
private static final long MAX_AGE_SECONDS = 3600;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
// 对所有路径生效
.addMapping("/**")
// 允许携带 Cookie 和认证信息
// 注意:设为 true 时,allowedOrigins 不能为 "*"
.allowCredentials(true)
// 允许的请求来源
// 方式1:使用 allowedOrigins 精确匹配
// .allowedOrigins("http://localhost:5173", "https://example.com")
// 方式2:使用 allowedOriginPatterns 支持通配符
.allowedOriginPatterns(allowedOrigins)
// 允许的 HTTP 方法
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
// 允许的请求头
.allowedHeaders("*")
// 或者精确指定:
// .allowedHeaders("Content-Type", "Authorization", "X-Requested-With")
// 允许前端访问的响应头
.exposedHeaders("X-Total-Count", "Content-Disposition")
// 预检请求缓存时间
.maxAge(MAX_AGE_SECONDS);
}
}
application.yml
cors:
allowed-origins: ${APP_CORS_ORIGINS:http://localhost:5173}
.env.dev
APP_CORS_ORIGINS=http://localhost:5173,http://localhost:3000
八、总结
- 开发环境:前端代理 + 后端 CORS 配置
- 生产环境:Nginx 反向代理 或 后端 CORS 配置
- 推荐方案:从配置文件/环境变量读取允许的域名
- 核心原则:跨域由后端通过响应头控制,前端无法绕过
项目分区导航:⬅️ 01-缓存使用最佳实践指南 | 02-Spring Boot 跨域(CORS)问题 | ➡️ 01-登录鉴权
💬 评论