JAVA REST API 跨域访问失败?CORS 配置陷阱与 Spring Security 解决方案

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 的核心在于浏览器和服务器之间的协商。当浏览器发起跨域请求时,它会根据请求的类型和内容,选择不同的处理方式。主要分为两种情况:

  1. 简单请求(Simple Request)

    简单请求满足以下所有条件:

    • 请求方法是 GETHEADPOST
    • 请求头中只包含以下字段:
      • Accept
      • Accept-Language
      • Content-Language
      • Content-Type (只限于 application/x-www-form-urlencoded, multipart/form-datatext/plain)
      • DPRDownlinkSave-DataViewport-WidthWidth

    对于简单请求,浏览器会直接发送请求,并在请求头中添加 Origin 字段,表明请求来源的域名。服务器收到请求后,如果允许该域名的访问,会在响应头中添加 Access-Control-Allow-Origin 字段,指定允许访问的域名。如果服务器不允许该域名的访问,则不添加该字段,浏览器会阻止响应数据的返回。

  2. 预检请求(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-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-Age。如果服务器不允许该请求,则返回错误状态码。

常见的 CORS 配置陷阱

在实际配置 CORS 时,很容易陷入一些陷阱,导致跨域访问失败。下面列举一些常见的错误:

  1. 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:8080https://localhost:8080 是不同的域名。
  2. 忘记处理预检请求

    如果你的 API 需要处理复杂的跨域请求(例如,使用 PUTDELETE 方法,或者包含自定义请求头),那么你必须正确处理预检请求。如果没有正确处理预检请求,浏览器会阻止真正的请求。

  3. Access-Control-Allow-MethodsAccess-Control-Allow-Headers 设置不完整

    Access-Control-Allow-Methods 必须包含所有实际请求使用的方法。Access-Control-Allow-Headers 必须包含所有实际请求使用的自定义请求头。如果缺少了任何一个,浏览器也会阻止请求。

  4. 缓存问题

    浏览器会对预检请求进行缓存,缓存时间由 Access-Control-Max-Age 决定。如果在缓存期间修改了 CORS 配置,可能会导致浏览器使用旧的配置,从而导致跨域访问失败。 解决办法是清除浏览器缓存,或者设置较短的 Access-Control-Max-Age

  5. 服务端配置优先级问题

    多个地方都配置了CORS,比如Filter,Interceptor,Controller的注解等,配置的优先级可能会导致预期外的结果。需要仔细梳理配置的生效顺序。

Spring Security 解决方案

Spring Security 提供了多种方式来解决 CORS 问题。我将介绍两种最常用的方法:

  1. 基于 @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。如果设置为 trueorigins 必须设置为具体的域名,不能使用 *
    • maxAge: 预检请求的有效期,单位是秒。

    优点:

    • 简单易用,配置灵活。

    缺点:

    • 需要在每个 Controller 类或方法上添加注解,代码冗余。
    • 配置分散,不易维护。
  2. 基于 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 中配置的规则,处理跨域请求。

配置的优先级:

  1. @CrossOrigin 注解的优先级高于 CorsConfigurationSource 如果一个 Controller 方法同时使用了 @CrossOrigin 注解和 CorsConfigurationSource,那么 @CrossOrigin 注解的配置会覆盖 CorsConfigurationSource 的配置。
  2. Filter 的顺序很重要。 确保 CorsFilter 在其他需要访问 Origin header 的 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.

解决方案:

  1. 后端配置:

    使用 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;
        }
    }
  2. 前端代码:

    确保前端代码正确设置了 Content-TypeAuthorization 头:

    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:8080setAllowedMethods 必须包含 POSTOPTIONSsetAllowedHeaders 必须包含 Content-TypeAuthorization
  • 前端代码必须正确设置 Content-TypeAuthorization 头。

通过以上配置,即可解决该场景下的跨域问题。

最后的思考

跨域问题是Web开发中不可避免的一个挑战。 理解CORS 的原理,熟悉常见的配置陷阱,并掌握 Spring Security 提供的解决方案,可以帮助我们更好地应对跨域问题,构建安全可靠的Web应用。

希望今天的讲解对大家有所帮助!

一些关键点回顾

  • CORS是为了安全,而不是为了阻止跨域。
  • 理解简单请求和预检请求的区别至关重要。
  • Spring Security 提供了灵活的 CORS 配置方式。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注