外观
Spring Boot 跨域处理
约 2201 字大约 7 分钟
2025-08-16
一、跨域基础概念
1. 什么是跨域
跨域:浏览器的同源策略限制了从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。
同源定义:协议、域名、端口三者完全相同。
示例:
http://example.com与https://example.com→ 跨域(协议不同)http://example.com与http://api.example.com→ 跨域(域名不同)http://example.com:8080与http://example.com:80→ 跨域(端口不同)
提示:跨域是浏览器的安全限制,服务器之间通信不存在跨域问题。
2. 为什么需要处理跨域
典型场景:
- 前后端分离架构(前端在本地开发服务器,后端在另一端口)
- 微服务架构中不同服务间的调用
- 第三方应用集成
- 移动应用通过 WebView 访问 Web API
不处理跨域的后果:
- 浏览器阻止请求
- 控制台显示错误:
No 'Access-Control-Allow-Origin' header is present - 前端无法获取响应数据
3. CORS 基础知识
CORS (Cross-Origin Resource Sharing):W3C 标准,通过 HTTP 头部实现跨域资源共享。
核心流程:
- 浏览器检测到跨域请求
- 对于非简单请求,先发送预检请求 (OPTIONS)
- 服务器响应预检请求
- 浏览器收到允许后,发送实际请求
简单请求 vs 非简单请求:
| 类型 | 条件 | 示例 |
|---|---|---|
| 简单请求 | 1. HTTP方法为GET、POST或HEAD 2. Content-Type为text/plain、multipart/form-data或application/x-www-form-urlencoded 3. 无自定义请求头 | GET /api/dataPOST /api/login (Content-Type: application/x-www-form-urlencoded) |
| 非简单请求 | 不满足简单请求条件 | PUT /api/dataPOST /api/data (Content-Type: application/json)带自定义请求头的请求 |
提示:90% 的 RESTful API 请求(尤其是使用 JSON 的 POST/PUT 请求)都是非简单请求,会触发预检请求。
二、Spring Boot 跨域解决方案
1. 全局配置 CORS(最常用)
实现方式:
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 GlobalCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 配置需要跨域的路径
.allowedOrigins("http://localhost:3000", "https://example.com") // 允许的源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法
.allowedHeaders("*") // 允许的请求头
.exposedHeaders("Authorization", "Link") // 暴露给前端的响应头
.allowCredentials(true) // 是否允许发送Cookie
.maxAge(3600); // 预检请求缓存时间(秒)
}
}关键参数说明:
| 方法 | 说明 | 示例 |
|---|---|---|
addMapping | 配置需要跨域的路径 | "/api/**" |
allowedOrigins | 允许的源(可多个) | "http://localhost:3000" |
allowedMethods | 允许的HTTP方法 | "GET", "POST", "PUT", "DELETE" |
allowedHeaders | 允许的请求头 | "*" 或 "Content-Type", "Authorization" |
exposedHeaders | 暴露给前端的响应头 | "Authorization", "X-Custom-Header" |
allowCredentials | 是否允许发送Cookie | true |
maxAge | 预检请求缓存时间 | 3600(1小时) |
提示:不要在生产环境使用
allowedOrigins("*"),这会带来安全风险。应明确指定允许的源。
2. 基于注解的 CORS 配置
实现方式:
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:3000",
maxAge = 3600,
allowedHeaders = {"Content-Type", "Authorization"},
exposedHeaders = "X-Request-ID")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
// ...
}
@PostMapping
@CrossOrigin(origins = "https://trusted.com",
methods = {RequestMethod.POST})
public User createUser(@RequestBody User user) {
// ...
}
}注解参数:
| 属性 | 说明 | 默认值 |
|---|---|---|
origins | 允许的源 | {"*"} |
methods | 允许的HTTP方法 | 所有方法 |
allowedHeaders | 允许的请求头 | 所有头 |
exposedHeaders | 暴露的响应头 | 空 |
allowCredentials | 是否允许发送Cookie | false |
maxAge | 预检请求缓存时间 | 1800秒 |
提示:注解方式适合特定控制器或方法的跨域配置,但不适合全局配置。对于统一API前缀的项目,全局配置更简洁。
3. 过滤器方式实现 CORS
实现方式:
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
response.setHeader("Access-Control-Expose-Headers", "X-Request-ID");
response.setHeader("Access-Control-Allow-Credentials", "true");
// 对于预检请求直接返回
if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
}适用场景:
- Spring Boot 版本较低(< 2.0)
- 需要更精细控制 CORS 行为
- 需要与其他过滤器协调
提示:在 Spring Boot 2.x 及以上版本,优先使用 WebMvcConfigurer 方式,更符合 Spring 风格。
4. Nginx 配置 CORS(备选方案)
实现方式:
server {
listen 80;
server_name api.example.com;
location / {
# CORS 配置
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always;
add_header 'Access-Control-Expose-Headers' 'X-Request-ID' always;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
}
proxy_pass http://backend;
}
}适用场景:
- 无法修改后端代码
- 需要在接入层统一处理跨域
- 有专门的 API 网关
提示:在前后端分离架构中,优先在应用层(Spring Boot)处理 CORS,而不是在 Nginx 层。应用层处理更灵活,可以与业务逻辑结合。
三、实用技巧与最佳实践
1. 动态源配置
问题:如何支持多个前端源,且源可能变化?
解决方案:
@Configuration
public class DynamicCorsConfig implements WebMvcConfigurer {
@Value("${cors.allowed-origins}")
private String allowedOrigins;
@Override
public void addCorsMappings(CorsRegistry registry) {
String[] origins = StringUtils.commaDelimitedListToStringArray(allowedOrigins);
registry.addMapping("/api/**")
.allowedOrigins(origins)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.allowCredentials(true)
.maxAge(3600);
}
}配置文件:
# application-prod.properties
cors.allowed-origins=https://example.com,https://app.example.com2. 生产环境安全配置
安全最佳实践:
@Configuration
public class SecureCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://example.com", "https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Content-Type", "Authorization", "X-Requested-With")
.exposedHeaders("X-Request-ID")
.allowCredentials(true)
.maxAge(3600);
}
}安全建议:
- 不要使用通配符:避免
allowedOrigins("*")(当allowCredentials=true时会失效) - 限制 HTTP 方法:只允许必要的方法
- 限制请求头:只允许必要的请求头
- 避免暴露敏感头:不要暴露包含敏感信息的响应头
- 设置合理的 maxAge:减少预检请求频率,但不要过长
3. 处理预检请求
问题:OPTIONS 请求被 Spring Security 拦截
解决方案:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 允许所有 OPTIONS 请求
.anyRequest().authenticated();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization"));
configuration.setExposedHeaders(Arrays.asList("X-Request-ID"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}4. 与 Spring Security 集成
完整集成示例:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors() // 启用 CORS 支持
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("https://example.com", "https://app.example.com"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization", "X-Requested-With"));
config.setExposedHeaders(Arrays.asList("X-Request-ID"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}四、常见问题与解决方案
1. 常见错误排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
No 'Access-Control-Allow-Origin' header | 未配置 CORS 或配置不正确 | 检查 CORS 配置,确保路径匹配 |
Credentials is not supported if the CORS header 'Access-Control-Allow-Origin' is '\*' | 使用 * 时设置了 allowCredentials=true | 指定具体源,不要使用通配符 |
Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers | 请求头未在 allowedHeaders 中声明 | 在配置中添加该请求头 |
Response to preflight request doesn't pass access control check: It does not have HTTP ok status | OPTIONS 请求未正确处理 | 确保预检请求返回 200 或 204 状态码 |
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include' | 同上 | 指定具体源 |
2. Spring Boot 版本兼容性
不同版本的差异:
Spring Boot 1.x:继承
WebMvcConfigurerAdapter@Configuration public class CorsConfig extends WebMvcConfigurerAdapter { @Override public void addCorsMappings(CorsRegistry registry) { // 配置 } }Spring Boot 2.x:实现
WebMvcConfigurer接口@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 配置 } }
提示:Spring Boot 2.0+ 已弃用
WebMvcConfigurerAdapter,应直接实现WebMvcConfigurer接口。
3. 生产环境问题
问题:本地开发正常,生产环境跨域失败
排查步骤:
- 检查生产环境 CORS 配置是否与开发环境一致
- 检查是否被 CDN 或 API 网关修改了响应头
- 检查是否有多个 CORS 配置冲突
- 使用浏览器开发者工具查看网络请求和响应头
- 检查 Spring Security 配置是否拦截了 OPTIONS 请求
五、实用技巧速查表
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 全局 API 跨域 | WebMvcConfigurer | 明确指定允许的源 |
| 特定控制器跨域 | @CrossOrigin | 适合少量特殊需求 |
| Spring Security 集成 | 配置 CorsConfigurationSource | 确保 OPTIONS 请求放行 |
| 动态源配置 | 从配置文件读取 | 使用逗号分隔多个源 |
| 生产环境安全 | 限制方法、头、源 | 避免使用通配符 |
| 预检请求问题 | 检查 OPTIONS 处理 | 确保返回 200/204 |
| 与认证集成 | allowCredentials=true | 配合 withCredentials 使用 |
| 暴露自定义头 | exposedHeaders | 前端才能获取到 |
