Spring Boot 中全面解决跨域请求
什么是跨域请求(CORS)
跨域的概念
跨域是指浏览器出于安全考虑,限制了从不同源(协议,域名,端口任一不同)的服务器请求资源,这是浏览器的同源策略(Same-Origin Policy)所导致的。
同源策略要求一下三个必须相同
- 协议相同(http/https)
- 域名相同
- 端口相同
为什么需要CORS
在实际开发中,前后端分离架构非常普遍,前端应用运行在一个域名下,后端 API 服务运行在另一个域名下,这时就会遇到跨域问题。
CORS(Cross-Origin Resource Sharing) 是一种 W3C 标准,允许服务器声明哪些源可以访问其资源,从而解决跨域问题。
CORS 的工作原理
简单请求与预检请求
浏览器将 CORS 请求分为两类:简单请求和预检请求。
简单请求条件:
- 方法为 GET、HEAD、POST 之一
- Content-Type 为 text/plain、multipart/form-data、application/x-www-form-urlencoded 之一
- 没有自定义头部
预检请求:
不满足简单请求条件的请求会先发送 OPTIONS 预检请求,获得服务器许可后再发送实际请求。
Spring Boot 中解决CORS的四种方式
方法一 使用@CrossOrigin 注解
在控制器方法上使用
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/users")
@CrossOrigin(origins = "http://localhost:3000")
public List<User> getUsers() {
return Arrays.asList(
new User(1, "张三", "zhangsan@example.com"),
new User(2, "李四", "lisi@example.com")
);
}
@PostMapping("/users")
@CrossOrigin(origins = "http://localhost:3000")
public User createUser(@RequestBody User user) {
// 创建用户逻辑
return user;
}
}
多路径模式配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// API 路径配置
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
// 管理后台路径配置
registry.addMapping("/admin/**")
.allowedOrigins("https://admin.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
.allowCredentials(true)
.maxAge(1800);
// 公开接口配置
registry.addMapping("/public/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD")
.allowedHeaders("Content-Type")
.maxAge(1800);
}
}
环境特定的配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Value("${cors.allowed.origins:http://localhost:3000}")
private String[] allowedOrigins;
@Value("${cors.allowed.methods:GET,POST,PUT,DELETE,OPTIONS}")
private String[] allowedMethods;
@Value("${cors.max.age:3600}")
private long maxAge;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(allowedOrigins)
.allowedMethods(allowedMethods)
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(maxAge);
}
}
对应的 application.yml 配置:
cors:
allowed:
origins:
- "http://localhost:3000"
- "https://staging.example.com"
- "https://app.example.com"
methods: "GET,POST,PUT,DELETE,OPTIONS"
max:
age: 3600
方法三 使用CorsFilter 过滤器
基础CorsFilter配置
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许的域名
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedOrigin("https://example.com");
// 允许的请求方法
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("OPTIONS");
// 允许的请求头
config.addAllowedHeader("*");
// 是否允许发送 Cookie
config.setAllowCredentials(true);
// 预检请求的有效期
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
高级CorsFilter配置
@Configuration
public class AdvancedCorsConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 从配置文件中读取允许的源
config.setAllowedOrigins(Arrays.asList(
"http://localhost:3000",
"https://admin.example.com",
"https://app.example.com"
));
// 设置允许的方法
config.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"
));
// 设置允许的头部
config.setAllowedHeaders(Arrays.asList(
"Authorization",
"Content-Type",
"X-Requested-With",
"Accept",
"Origin",
"Access-Control-Request-Method",
"Access-Control-Request-Headers",
"X-CSRF-TOKEN"
));
// 设置暴露的头部
config.setExposedHeaders(Arrays.asList(
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"X-Custom-Header"
));
// 允许凭证
config.setAllowCredentials(true);
// 预检请求缓存时间
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 为不同路径设置不同的 CORS 配置
source.registerCorsConfiguration("/api/**", config);
CorsConfiguration publicConfig = new CorsConfiguration();
publicConfig.addAllowedOrigin("*");
publicConfig.addAllowedMethod("GET");
publicConfig.addAllowedHeader("Content-Type");
source.registerCorsConfiguration("/public/**", publicConfig);
return new CorsFilter(source);
}
}
方式四 手动处理OPTIONS请求
在某些特殊情况下,你可能需要手动处理预检请求:
@Component
public class CustomCorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
// 设置 CORS 头部
response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
// 如果是 OPTIONS 请求,直接返回 200
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {
// 初始化逻辑
}
@Override
public void destroy() {
// 清理逻辑
}
}
高级配置和最佳实践
环境特定的cors配置
@Configuration
@Profile("dev")
public class DevCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000", "http://localhost:8080")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}
@Configuration
@Profile("prod")
public class ProdCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com", "https://admin.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
.allowCredentials(true)
.maxAge(3600);
}
}
动态CORS配置
@Component
public class DynamicCorsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String origin = request.getHeader("Origin");
if (isAllowedOrigin(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Requested-With");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
}
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
filterChain.doFilter(request, response);
}
private boolean isAllowedOrigin(String origin) {
// 从数据库或配置中心动态获取允许的源
List<String> allowedOrigins = Arrays.asList(
"http://localhost:3000",
"https://app.example.com",
"https://admin.example.com"
);
return allowedOrigins.contains(origin);
}
}
安全最佳实践
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and() // 启用 CORS 支持
.csrf().disable() // 根据需求决定是否禁用 CSRF
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.httpBasic();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// 生产环境中应该从配置文件中读取
configuration.setAllowedOrigins(Arrays.asList(
"https://trusted-domain.com",
"https://app.trusted-domain.com"
));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
configuration.setExposedHeaders(Arrays.asList("X-Custom-Header"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}
测试CORS配置
测试控制器
@RestController
@RequestMapping("/api/test")
public class CorsTestController {
@GetMapping("/cors-test")
@CrossOrigin(origins = "http://localhost:3000")
public ResponseEntity<Map<String, String>> corsTest() {
Map<String, String> response = new HashMap<>();
response.put("message", "CORS test successful");
response.put("timestamp", Instant.now().toString());
return ResponseEntity.ok(response);
}
@PostMapping("/cors-post")
public ResponseEntity<Map<String, Object>> corsPostTest(@RequestBody TestRequest request) {
Map<String, Object> response = new HashMap<>();
response.put("received", request);
response.put("processedAt", Instant.now().toString());
return ResponseEntity.ok(response);
}
@PutMapping("/cors-put/{id}")
public ResponseEntity<Map<String, Object>> corsPutTest(
@PathVariable String id,
@RequestBody TestRequest request) {
Map<String, Object> response = new HashMap<>();
response.put("id", id);
response.put("data", request);
response.put("updatedAt", Instant.now().toString());
return ResponseEntity.ok(response);
}
}
class TestRequest {
private String name;
private String email;
// getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
前端测试代码
<!DOCTYPE html>
<html>
<head>
<title>CORS Test</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<h1>CORS Test Page</h1>
<button onclick="testGet()">Test GET</button>
<button onclick="testPost()">Test POST</button>
<button onclick="testPut()">Test PUT</button>
<div id="result"></div>
<script>
const API_BASE = 'http://localhost:8080/api/test';
async function testGet() {
try {
const response = await axios.get(`${API_BASE}/cors-test`, {
withCredentials: true
});
document.getElementById('result').innerHTML =
`<pre>${JSON.stringify(response.data, null, 2)}</pre>`;
} catch (error) {
document.getElementById('result').innerHTML =
`<pre style="color: red">Error: ${error.message}</pre>`;
}
}
async function testPost() {
try {
const response = await axios.post(`${API_BASE}/cors-post`, {
name: 'Test User',
email: 'test@example.com'
}, {
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
});
document.getElementById('result').innerHTML =
`<pre>${JSON.stringify(response.data, null, 2)}</pre>`;
} catch (error) {
document.getElementById('result').innerHTML =
`<pre style="color: red">Error: ${error.message}</pre>`;
}
}
async function testPut() {
try {
const response = await axios.put(`${API_BASE}/cors-put/123`, {
name: 'Updated User',
email: 'updated@example.com'
}, {
withCredentials: true,
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'custom-value'
}
});
document.getElementById('result').innerHTML =
`<pre>${JSON.stringify(response.data, null, 2)}</pre>`;
} catch (error) {
document.getElementById('result').innerHTML =
`<pre style="color: red">Error: ${error.message}</pre>`;
}
}
</script>
</body>
</html>
常见问题与解决方案
CORS配置不生效
问题原因:
- 配置顺序问题
- 过滤器链顺序问题
- 安全配置冲突
解决方案:
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE) // 确保高优先级
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
}
携带凭证(Credentials)问题
当使用 allowCredentials(true) 时,不能使用通配符 * 作为允许的源:
// 错误配置
.config.setAllowedOrigins(Arrays.asList("*")); // 与 allowCredentials(true) 冲突
// 正确配置
config.setAllowedOrigins(Arrays.asList("http://localhost:3000", "https://example.com"));
总结
Spring Boot 提供了多种灵活的方式来处理 CORS 跨域请求:
-
@CrossOrigin 注解:适合细粒度的控制器或方法级别配置
-
WebMvcConfigurer:适合应用级别的全局配置
-
CorsFilter:提供最灵活的过滤器级别配置
-
手动处理:适合特殊需求的定制化处理
选择建议:
- 开发环境:使用宽松的全局配置
- 生产环境:使用严格的、基于配置的 CORS 策略
- 微服务架构:在 API 网关层统一处理 CORS
- 特殊需求:使用 CorsFilter 进行精细控制

浙公网安备 33010602011771号