当前位置: 首页 > news >正文

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 {@Overridepublic 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;@Overridepublic 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 {@Beanpublic 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("*");// 是否允许发送 Cookieconfig.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 {@Overridepublic 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 请求,直接返回 200if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {response.setStatus(HttpServletResponse.SC_OK);return;}chain.doFilter(req, res);}@Overridepublic void init(FilterConfig filterConfig) {// 初始化逻辑}@Overridepublic void destroy() {// 清理逻辑}
}

高级配置和最佳实践

环境特定的cors配置

@Configuration
@Profile("dev")
public class DevCorsConfig implements WebMvcConfigurer {@Overridepublic 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 {@Overridepublic 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 {@Overrideprotected 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 {@Overrideprotected 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();}@Beanpublic 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 setterspublic 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 {@Overridepublic 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 进行精细控制
http://www.hskmm.com/?act=detail&tid=36939

相关文章:

  • OpenTelemetry语义约定:规范可观测性数据,提升系统洞察力
  • 拓展欧几里得算法
  • 两两交换链表中的节点-leetcode
  • 算法第二章实践作业
  • 解决homebrew下载报错问题
  • 软考中级学习总结(5)
  • 软考中级学习总结(4)
  • 每日反思(2025_10_22)
  • docker: Error response from daemon: failed to set up container networking 解决办法
  • 实验2 现代C++编程初体验
  • CSP-S36
  • 新学期每日总结(第13天)
  • GCM(Galois/Counter Mode) 认证加密算法实现
  • 【学习笔记】slope-trick
  • 10.13-10.19学习做题笔记
  • 2025.10.22
  • yny计数题记录
  • 20232404 2025-2026-2 《网络与系统攻防技术》实验二实验报告
  • 1020302118兰逸霏的第一次作业
  • ubuntu 25.10 修改源 - ldx
  • pytorch学习笔记(1)
  • 20232318 2025-2026-1 《网络与系统攻防技术》实验二实验报告
  • 《中华人民共和国网络安全法》第二十一条这一核心考点
  • 嵌入式软件分层架构设计 - lucky
  • DP 基础题乱做
  • [题解]P4616 [COCI 2017/2018 #5] Pictionary
  • 二三级区别
  • 第九章-Where-1S-tHe-Hacker
  • CF 2023D Many Games
  • 2025.10.22考试记录