JAVA REST API 跨域访问失败?CORS 配置陷阱与 Spring Security 解决方案
各位同学们,大家好!今天我们来聊聊Java REST API开发中经常遇到的一个问题:跨域访问失败(CORS)。这个问题看似简单,但实际配置起来却可能充满陷阱。我会从CORS的概念、原理,到常见的配置错误,再到如何利用Spring Security优雅地解决跨域问题,给大家做一个深入的讲解。
什么是跨域?为什么要关注它?
首先,我们需要明确什么是跨域。跨域,全称Cross-Origin Resource Sharing,指的是浏览器出于安全考虑,对从一个域名的网页去请求另一个域名的资源的行为进行限制。这个“域名”包括协议(protocol)、域名(domain)和端口(port),只要这三者中有一个不同,就认为是不同的域。
举个例子:
- 你的前端应用运行在 
http://localhost:8080 - 你的后端 API 运行在 
http://localhost:9000 
由于端口不同,这两个地址属于不同的域。如果前端应用直接使用JavaScript发起请求到后端API,浏览器会阻止这个请求,这就是典型的跨域问题。
为什么要有跨域限制?
跨域限制主要是为了防止恶意网站通过脚本获取用户敏感信息。设想一下,如果你登录了银行网站,恶意网站可以通过 JavaScript 访问你的银行账户信息,这将是非常危险的。
CORS 的作用是什么?
CORS 是一种机制,允许服务器声明哪些源(origin)可以访问它的资源,从而在保证安全的前提下,实现跨域请求。换句话说,CORS 不是为了阻止跨域请求,而是为了在控制下允许跨域请求。
CORS 的工作原理
CORS 的核心在于浏览器和服务器之间的协商。当浏览器发起跨域请求时,它会根据请求的类型和内容,选择不同的处理方式。主要分为两种情况:
- 
简单请求(Simple Request)
简单请求满足以下所有条件:
- 请求方法是 
GET、HEAD或POST - 请求头中只包含以下字段:
AcceptAccept-LanguageContent-LanguageContent-Type(只限于application/x-www-form-urlencoded,multipart/form-data或text/plain)DPR、Downlink、Save-Data、Viewport-Width、Width
 
对于简单请求,浏览器会直接发送请求,并在请求头中添加
Origin字段,表明请求来源的域名。服务器收到请求后,如果允许该域名的访问,会在响应头中添加Access-Control-Allow-Origin字段,指定允许访问的域名。如果服务器不允许该域名的访问,则不添加该字段,浏览器会阻止响应数据的返回。 - 请求方法是 
 - 
预检请求(Preflight Request)
如果请求不满足简单请求的条件,浏览器会先发送一个
OPTIONS请求,称为预检请求。预检请求用于询问服务器是否允许真正的请求。预检请求的请求头包含以下字段:
Origin: 请求来源的域名Access-Control-Request-Method: 实际请求的方法Access-Control-Request-Headers: 实际请求包含的自定义请求头
服务器收到预检请求后,必须返回一个响应,包含以下字段:
Access-Control-Allow-Origin: 允许访问的域名,可以是*或者具体的域名。Access-Control-Allow-Methods: 允许的请求方法,例如GET, POST, PUT, DELETE。Access-Control-Allow-Headers: 允许的自定义请求头,例如Content-Type, Authorization。Access-Control-Max-Age: 预检请求的有效期,单位是秒。
如果服务器不允许该请求,则返回错误状态码,浏览器会阻止真正的请求。
 
总结一下:
| 请求类型 | 浏览器行为 | 服务器行为 | 
|---|---|---|
| 简单请求 | 直接发送请求,添加 Origin 头 | 
如果允许该域名访问,响应头添加 Access-Control-Allow-Origin。如果不允许,不添加该字段。 | 
| 预检请求 | 先发送 OPTIONS 请求(预检请求) | 
响应预检请求,包含 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers 和 Access-Control-Max-Age。如果服务器不允许该请求,则返回错误状态码。 | 
常见的 CORS 配置陷阱
在实际配置 CORS 时,很容易陷入一些陷阱,导致跨域访问失败。下面列举一些常见的错误:
- 
Access-Control-Allow-Origin设置错误这是最常见的错误。
Access-Control-Allow-Origin必须设置正确的域名,才能允许该域名的访问。- 误用 
null: 有些开发者可能会将Access-Control-Allow-Origin设置为null。这通常是因为请求的Origin头是null,例如从本地文件(file://)发起的请求。但是,将Access-Control-Allow-Origin设置为null并不能解决这个问题,反而会导致其他问题。 - *使用 `
的风险:**表示允许所有域名访问,这在开发阶段可能很方便,但在生产环境中非常危险。因为它会允许任何网站访问你的 API,存在安全风险。 特别是涉及到cookie认证,如果使用了Access-Control-Allow-Credentials: true, 那么Access-Control-Allow-Origin不能使用`, 必须指定明确的域名。 - 忘记添加协议或端口:  
Access-Control-Allow-Origin必须包含完整的域名,包括协议和端口。例如,http://localhost:8080和https://localhost:8080是不同的域名。 
 - 误用 
 - 
忘记处理预检请求
如果你的 API 需要处理复杂的跨域请求(例如,使用
PUT、DELETE方法,或者包含自定义请求头),那么你必须正确处理预检请求。如果没有正确处理预检请求,浏览器会阻止真正的请求。 - 
Access-Control-Allow-Methods和Access-Control-Allow-Headers设置不完整Access-Control-Allow-Methods必须包含所有实际请求使用的方法。Access-Control-Allow-Headers必须包含所有实际请求使用的自定义请求头。如果缺少了任何一个,浏览器也会阻止请求。 - 
缓存问题
浏览器会对预检请求进行缓存,缓存时间由
Access-Control-Max-Age决定。如果在缓存期间修改了 CORS 配置,可能会导致浏览器使用旧的配置,从而导致跨域访问失败。 解决办法是清除浏览器缓存,或者设置较短的Access-Control-Max-Age。 - 
服务端配置优先级问题
多个地方都配置了CORS,比如Filter,Interceptor,Controller的注解等,配置的优先级可能会导致预期外的结果。需要仔细梳理配置的生效顺序。
 
Spring Security 解决方案
Spring Security 提供了多种方式来解决 CORS 问题。我将介绍两种最常用的方法:
- 
基于
@CrossOrigin注解@CrossOrigin注解可以用于 Controller 类或方法上,用于配置 CORS。import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @CrossOrigin(origins = "http://localhost:8080", allowedHeaders = "*") // 允许来自 http://localhost:8080 的跨域请求 public class MyController { @GetMapping("/api/hello") public String hello() { return "Hello, world!"; } }@CrossOrigin注解的常用属性:origins: 允许访问的域名,可以是一个字符串,也可以是一个字符串数组。originPatterns: 使用 Ant 风格的路径匹配,允许访问的域名。methods: 允许的请求方法,可以是一个 RequestMethod 数组。allowedHeaders: 允许的自定义请求头,可以是一个字符串数组。exposedHeaders: 允许浏览器访问的响应头,可以是一个字符串数组。allowCredentials: 是否允许携带 Cookie,默认为false。如果设置为true,origins必须设置为具体的域名,不能使用*。maxAge: 预检请求的有效期,单位是秒。
优点:
- 简单易用,配置灵活。
 
缺点:
- 需要在每个 Controller 类或方法上添加注解,代码冗余。
 - 配置分散,不易维护。
 
 - 
基于
CorsConfigurationSource的全局配置Spring Security 提供了
CorsConfigurationSource接口,用于配置全局的 CORS 规则。我们可以自定义一个CorsConfigurationSource,并将其注册到 Spring Security 中。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors() .and() .csrf().disable() // 禁用 CSRF,方便测试 .authorizeHttpRequests() .anyRequest().permitAll(); // 允许所有请求 return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080")); // 允许来自 http://localhost:8080 的跨域请求 configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许所有方法 configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization")); // 允许的头 configuration.setAllowCredentials(true); // 允许携带cookie UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); // 拦截所有请求 return source; } }优点:
- 集中配置,易于维护。
 - 可以灵活地配置 CORS 规则。
 
缺点:
- 配置相对复杂。
 
 
选择哪种方式?
- 如果你的 API 只需要处理简单的跨域请求,并且只需要对少数几个 Controller 进行配置,那么可以使用 
@CrossOrigin注解。 - 如果你的 API 需要处理复杂的跨域请求,或者需要对大量的 Controller 进行配置,那么建议使用 
CorsConfigurationSource进行全局配置。 
深入理解 Spring Security CORS 配置
Spring Security 的 CORS 支持实际上是基于 org.springframework.web.filter.CorsFilter 实现的。当我们配置了 CorsConfigurationSource 后,Spring Security 会自动创建一个 CorsFilter,并将其添加到 Filter 链中。CorsFilter 会拦截所有请求,并根据 CorsConfigurationSource 中配置的规则,处理跨域请求。
配置的优先级:
@CrossOrigin注解的优先级高于CorsConfigurationSource。 如果一个 Controller 方法同时使用了@CrossOrigin注解和CorsConfigurationSource,那么@CrossOrigin注解的配置会覆盖CorsConfigurationSource的配置。- Filter 的顺序很重要。 确保 
CorsFilter在其他需要访问Originheader 的 Filter 之前执行。通常情况下,Spring Security 会自动处理 Filter 的顺序,但如果你的应用使用了自定义的 Filter,需要注意 Filter 的顺序。 
一些高级用法:
- 动态配置 
Access-Control-Allow-Origin: 有时候,我们需要根据请求的Origin头动态地设置Access-Control-Allow-Origin。例如,我们可以从数据库中读取允许访问的域名列表,然后根据请求的Origin头,判断是否允许该域名的访问。 - 基于环境的配置: 我们可以根据不同的环境(例如,开发环境、测试环境、生产环境)使用不同的 CORS 配置。例如,在开发环境中,我们可以允许所有域名访问,而在生产环境中,只允许特定的域名访问。
 
案例分析:解决实际问题
现在,让我们通过一个实际的案例来演示如何解决跨域问题。
场景:
- 前端应用运行在 
http://localhost:8080 - 后端 API 运行在 
http://localhost:9000 - 前端应用需要通过 
POST请求向后端 API 发送 JSON 数据,并携带Authorization头。 
问题:
前端应用无法访问后端 API,浏览器报错:No 'Access-Control-Allow-Origin' header is present on the requested resource.
解决方案:
- 
后端配置:
使用
CorsConfigurationSource进行全局配置:import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors() .and() .csrf().disable() // 禁用 CSRF,方便测试 .authorizeHttpRequests() .anyRequest().permitAll(); // 允许所有请求 return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080")); // 允许来自 http://localhost:8080 的跨域请求 configuration.setAllowedMethods(Arrays.asList("POST", "OPTIONS")); // 允许 POST 和 OPTIONS 方法 configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization")); // 允许 Content-Type 和 Authorization 头 configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } } - 
前端代码:
确保前端代码正确设置了
Content-Type和Authorization头:fetch('http://localhost:9000/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer your_token' }, body: JSON.stringify({ message: 'Hello from frontend!' }) }) .then(response => response.json()) .then(data => console.log(data)); 
分析:
- 由于使用了 
POST方法和Authorization头,这是一个复杂的跨域请求。 - 后端配置中,
setAllowedOrigins必须设置为http://localhost:8080,setAllowedMethods必须包含POST和OPTIONS,setAllowedHeaders必须包含Content-Type和Authorization。 - 前端代码必须正确设置 
Content-Type和Authorization头。 
通过以上配置,即可解决该场景下的跨域问题。
最后的思考
跨域问题是Web开发中不可避免的一个挑战。 理解CORS 的原理,熟悉常见的配置陷阱,并掌握 Spring Security 提供的解决方案,可以帮助我们更好地应对跨域问题,构建安全可靠的Web应用。
希望今天的讲解对大家有所帮助!
一些关键点回顾
- CORS是为了安全,而不是为了阻止跨域。
 - 理解简单请求和预检请求的区别至关重要。
 - Spring Security 提供了灵活的 CORS 配置方式。