1. 项目概述:从“跨域错误”到CORS配置

相信不少Java后端开发者,尤其是刚接触前后端分离项目时,都遇到过那个经典的浏览器控制台错误: Access to fetch at ‘http://api.example.com‘ from origin ‘http://localhost:8080‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin‘ header is present on the requested resource. 。这个令人头疼的“跨域”问题,几乎是现代Web开发的必经之路。它并非服务器拒绝响应,而是浏览器出于安全考虑,主动拦截了来自不同源的响应,防止恶意网站窃取数据。我们今天要深入探讨的,就是在Java生态中,如何正确、优雅且安全地配置CORS(跨域资源共享),让前端应用能顺利调用后端API。

CORS本身是一套W3C标准,它允许服务器声明哪些“外域”有权访问自己的资源。对于Java开发者而言,实现CORS不仅仅是加几个响应头那么简单。它涉及到对不同HTTP方法的处理(特别是OPTIONS预检请求)、对携带凭证(如Cookie)请求的支持,以及如何在便捷与安全之间找到平衡点。一个配置不当的CORS策略,轻则导致功能异常,重则可能引入严重的安全漏洞,例如将内部API暴露给任意网站。因此,理解其原理并掌握在Spring Boot、Servlet Filter等常见场景下的配置方法,是每一位Java Web开发者必须掌握的技能。接下来,我将结合多年项目实战经验,为你拆解从基础配置到高级安全考量的完整方案。

2. CORS核心原理与工作机制拆解

在动手写代码之前,我们必须先搞清楚浏览器和服务器之间到底发生了什么。很多开发者尝试配置CORS失败,根源在于对机制理解不透彻。CORS的交互主要分为两种场景: 简单请求 预检请求 。理解它们的区别,是成功配置的第一步。

2.1 简单请求与非简单请求的判定

浏览器并非对所有跨域请求都“一视同仁”。为了效率,它定义了一类“简单请求”,这类请求可以直接发出,服务器通过响应头来控制是否允许跨域。一个请求必须同时满足以下所有条件,才是简单请求:

  1. 方法限制 :仅限 GET HEAD POST
  2. 请求头限制 :除了浏览器自动设置的头部(如 Connection User-Agent 等),只能包含以下安全列表中的头部: Accept Accept-Language Content-Language Content-Type (但值仅限于 application/x-www-form-urlencoded multipart/form-data text/plain )。
  3. 请求中的任意XMLHttpRequestUpload对象均没有注册任何事件监听器

如果你的请求使用了 PUT DELETE 方法,或者 Content-Type application/json ,或者你自定义了 Authorization X-Requested-With 等头部,那么这个请求就属于 非简单请求

对于简单请求,浏览器会直接发出请求,并在响应中检查是否包含 Access-Control-Allow-Origin 头部。如果包含且值匹配请求源(或为 * ),则允许跨域访问响应数据。

2.2 预检请求的完整流程

对于非简单请求,浏览器不会直接发出实际请求(比如你的 POST /api/user )。它会先自动发起一个 OPTIONS 方法的 预检请求 到同一个URL。这个预检请求的目的是“询问”服务器:“我来自 http://localhost:8080 ,想用 POST 方法,携带 Content-Type: application/json Authorization 头,你允许吗?”

预检请求会携带几个关键头部:

  • Origin : 请求来源。
  • Access-Control-Request-Method : 实际请求将要使用的方法(如 POST )。
  • Access-Control-Request-Headers : 实际请求将要携带的自定义头部列表(如 authorization, content-type )。

服务器必须正确响应这个OPTIONS请求。 响应中需要包含:

  • Access-Control-Allow-Origin : 允许的源。
  • Access-Control-Allow-Methods : 允许的方法列表。
  • Access-Control-Allow-Headers : 允许的请求头列表。
  • Access-Control-Max-Age : (可选)指定预检请求的结果可以被缓存多久(秒),减少后续请求的预检次数。

只有预检请求的响应通过了浏览器的检查,浏览器才会接着发出真正的实际请求。如果预检请求失败(比如服务器没处理OPTIONS,或者返回的允许头/方法不匹配),你会看到那个经典的 has been blocked by CORS policy: Response to preflight request doesn‘t pass access control check 错误,而你的实际请求代码甚至不会被执行。

注意 :很多新手配置时只关注了实际请求的响应头,却完全忽略了处理 OPTIONS 预检请求,这是导致配置失败的最常见原因。你的后端应用必须能正确处理 OPTIONS 方法的请求并返回正确的CORS头部。

3. Spring Boot中的CORS配置方案详解

Spring Boot作为Java生态的绝对主流,提供了多种灵活配置CORS的方式。从全局配置到细粒度控制,我们需要根据项目需求选择合适的方法。

3.1 使用 @CrossOrigin 注解进行控制器级别配置

这是最快速、最直观的方式,适合对单个或某几个控制器进行跨域配置。你可以将注解加在控制器类上或具体的方法上。

@RestController
@RequestMapping("/api")
// 在类上配置,对该控制器所有方法生效
@CrossOrigin(origins = "http://localhost:8080", maxAge = 3600)
public class UserController {

    @GetMapping("/user/{id}")
    // 方法上的注解会覆盖类上的配置
    @CrossOrigin(origins = {"http://localhost:8080", "https://admin.example.com"})
    public User getUser(@PathVariable Long id) {
        // ...
    }

    @PostMapping("/user")
    // 允许所有源,生产环境慎用!
    @CrossOrigin(origins = "*")
    public User createUser(@RequestBody User user) {
        // ...
    }
}

参数解析与实操心得

  • origins / value : 允许的源列表。 强烈不建议在生产环境使用 "*" ,这等同于完全开放,存在安全风险。应明确列出前端应用的部署地址。
  • allowedHeaders : 允许的请求头。如果你的前端请求会携带 Authorization X-Token 等自定义头,必须在这里列出。默认已包含 Origin , Accept , Content-Type 等简单头部。
  • exposedHeaders : 除了CORS安全响应头( Cache-Control , Content-Language 等)之外,你希望浏览器能访问到的额外响应头。例如,如果你的API返回一个自定义分页头 X-Total-Count ,就需要在这里暴露。
  • methods : 允许的HTTP方法,默认是注解所映射的方法(如 @GetMapping 对应 GET )。
  • allowCredentials : 布尔值,是否允许发送Cookie等凭证信息。 如果设置为 true ,则 origins 不能为 "*" ,必须指定明确的域名 ,否则浏览器会拒绝请求。
  • maxAge : 预检请求缓存时间,单位秒。设置一个合理的值(如1800)可以减少不必要的OPTIONS请求,提升性能。

踩坑记录 :我曾在一个项目中将 allowCredentials 设为 true ,但 origins 配置了 * ,结果前端始终无法携带Cookie。排查了很久才发现是浏览器遵循规范拒绝了这种不安全的组合。务必记住这个规则。

3.2 实现 WebMvcConfigurer 进行全局配置

当你的项目中有大量API需要统一的CORS策略时,在每一个控制器上添加注解就显得非常冗余。此时,实现 WebMvcConfigurer 接口并重写 addCorsMappings 方法是更优雅的全局解决方案。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**") // 匹配的路径模式
                .allowedOrigins("http://localhost:8080", "https://your-frontend.com") // 允许的源
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD") // 允许的方法
                .allowedHeaders("*") // 允许所有请求头,生产环境建议明确列出
                .exposedHeaders("X-Total-Count", "X-Custom-Token") // 暴露的响应头
                .allowCredentials(true) // 允许凭证
                .maxAge(3600); // 预检缓存1小时

        // 可以为不同的路径配置不同的策略
        registry.addMapping("/public/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD")
                .allowCredentials(false);
    }
}

全局配置的优势与注意事项

  • 优势 :配置集中,管理方便,避免了注解的分散。对于RESTful API项目,这通常是首选。
  • 注意路径匹配 addMapping 中的Ant风格路径模式(如 /api/** )要能覆盖所有需要跨域的接口。小心不要遗漏某些路径。
  • allowedHeaders(“*”) 的风险 :虽然方便,但意味着接受任何请求头。在安全要求高的场景,应像 allowedOrigins 一样,明确列出需要的头部,如 “Authorization“, “Content-Type“, “X-Requested-With“
  • @CrossOrigin 的优先级 :如果同时使用了全局配置和 @CrossOrigin 注解, 注解的配置会覆盖全局配置 。这可以用于为特定接口设置更宽松或更严格的策略。

3.3 使用 CorsFilter 进行更底层的控制

WebMvcConfigurer 的方式在大多数Spring MVC场景下工作良好。但在一些更复杂的情况下,比如需要处理静态资源、或者与Spring Security集成时遇到优先级问题,或者你使用的是Spring WebFlux(响应式编程),直接配置一个 CorsFilter Bean可能是更可靠的选择。

@Configuration
public class CorsConfiguration {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();

        // 配置策略
        config.setAllowCredentials(true); // 允许凭证
        config.addAllowedOrigin("http://localhost:8080"); // 注意:不能同时使用setAllowCredentials(true)和addAllowedOrigin("*")
        config.addAllowedOrigin("https://your-frontend.com");
        config.addAllowedHeader("*"); // 允许所有头
        config.addAllowedMethod("*"); // 允许所有方法
        config.setMaxAge(3600L); // 缓存时间
        config.addExposedHeader("X-Total-Count"); // 暴露自定义头

        // 注册CORS配置,应用到所有路径
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

为什么选择CorsFilter?

  1. 优先级更高 CorsFilter 是一个Servlet Filter,它在请求处理链的早期执行,能确保在所有Spring MVC处理之前就处理好CORS头部,避免与其他Filter(如Spring Security的过滤器链)冲突。
  2. 更广泛的适用性 :它对所有经过Servlet容器的请求都生效,包括静态资源请求、错误页面等,而 WebMvcConfigurer 主要作用于DispatcherServlet分发的请求。
  3. 与Spring Security集成 :当项目使用Spring Security时,有时CORS配置会失效,因为Security的过滤器链可能先于CORS逻辑执行并返回了403错误。此时,将 CorsFilter 的Bean定义放在Security配置类之前,或者明确在Security配置中调用 .cors() 并提供一个 CorsConfigurationSource ,是更稳妥的做法。

一个与Spring Security集成的常见配置示例

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors() // 启用CORS支持,会从Spring容器中查找名为`corsFilter`的Bean或`CorsConfigurationSource`
                .and()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .csrf().disable(); // 注意:在前后端分离且使用Token等无状态认证时,通常可以禁用CSRF
    }

    // 显式提供CorsConfigurationSource Bean
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

4. 传统Servlet/Filter项目中的CORS实现

如果你的项目没有使用Spring Boot,而是基于原生的Servlet API或更轻量的框架(如Jersey、Spark等),你需要手动编写Filter来处理CORS。这是理解CORS机制最直接的方式。

4.1 手动实现CORS Filter

下面是一个功能完整的CORS Filter实现示例:

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class SimpleCorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 1. 设置允许的源(根据需求动态或静态配置)
        String allowedOrigin = "http://localhost:8080";
        response.setHeader("Access-Control-Allow-Origin", allowedOrigin);

        // 2. 处理预检请求(OPTIONS)
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            // 允许的方法
            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");
            // 预检请求直接返回成功,无需继续执行过滤器链
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        // 3. 对于非OPTIONS请求,继续设置其他CORS头并传递请求
        response.setHeader("Access-Control-Allow-Credentials", "true");
        // 可以暴露自定义响应头
        response.setHeader("Access-Control-Expose-Headers", "X-Custom-Header");

        // 4. 继续执行过滤器链
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void destroy() {}
}

关键点解析

  • 区分请求类型 :核心逻辑在于判断请求方法是否为 OPTIONS 。如果是,则直接构造一个包含所有必要CORS头部的成功响应并返回, 不再调用 chain.doFilter() ,因为预检请求不需要执行实际的业务逻辑。
  • Access-Control-Allow-Credentials :当需要前端传递Cookie或Authorization头进行认证时,此头必须设置为 true ,且 Access-Control-Allow-Origin 不能为 *
  • Access-Control-Expose-Headers :默认情况下,浏览器只能访问CORS安全列表中的响应头。如果你在响应中设置了自定义头(如分页信息 X-Total-Count ),需要在此列出,前端JavaScript才能通过 getResponseHeader() 读取。

4.2 在web.xml中注册Filter

编写好Filter后,需要在 web.xml 中配置其映射关系,确保它能拦截到所有需要跨域处理的请求。

<web-app>
    <filter>
        <filter-name>SimpleCorsFilter</filter-name>
        <filter-class>com.yourpackage.SimpleCorsFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>SimpleCorsFilter</filter-name>
        <url-pattern>/api/*</url-pattern> <!-- 只拦截API路径 -->
        <!-- 或者拦截所有请求:<url-pattern>/*</url-pattern> -->
    </filter-mapping>
</web-app>

部署与调试心得 :手动实现Filter时,最容易出错的地方就是响应头的设置不全或逻辑错误。务必使用浏览器的开发者工具(Network标签)仔细检查 预检请求(OPTIONS) 实际请求 的请求头和响应头是否完全匹配。特别是 Access-Control-Allow-Headers ,一定要包含前端实际发送的所有自定义头名称( 大小写敏感 )。

5. CORS配置中的关键安全考量与最佳实践

配置CORS不仅仅是为了让前端能访问到API,更重要的是在开放访问的同时,筑起一道安全防线。一个 Access-Control-Allow-Origin: * 的配置,可能就是你系统最大的漏洞之一。

5.1 源(Origin)白名单:严禁使用通配符“*”

这是CORS安全中最重要的一条原则。 Access-Control-Allow-Origin: * 意味着任何网站都可以通过浏览器脚本访问你的API。如果你的API涉及用户数据、管理功能或任何敏感操作,这等同于完全不设防。

正确做法 :建立严格的白名单机制。

  • Spring Boot示例(动态白名单) :可以从数据库或配置中心读取允许的域名列表,在 CorsConfigurationSource 或Filter中动态判断。
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
    
        // 假设从环境变量或配置类中获取白名单
        List<String> allowedOrigins = Arrays.asList(env.getProperty("cors.allowed.origins", "").split(","));
        config.setAllowedOrigins(allowedOrigins);
        // ... 其他配置
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
    
  • 生产环境建议 :将白名单配置在外部配置文件(如 application-prod.yml )或环境变量中,便于不同环境(开发、测试、生产)独立管理。

5.2 限制允许的HTTP方法与请求头

不要盲目地允许所有方法( * )和所有请求头( * )。应根据API的实际功能,遵循最小权限原则。

  • 方法限制 :一个只读的查询API,只允许 GET HEAD 即可。
  • 请求头限制 :明确列出前端应用会使用的头部,如 Authorization , Content-Type , X-Requested-With 。这可以防止攻击者利用未经验证的自定义头进行攻击。

5.3 谨慎处理凭证与敏感信息

Access-Control-Allow-Credentials: true 时,浏览器会将Cookie等凭证信息随请求发送。这在与基于Session的认证结合时很常见。但请务必注意:

  1. Origin 不能为 * :这是浏览器的强制规定。
  2. 响应中的敏感信息 :即使通过了CORS检查,也要确保API本身有完善的权限验证(如检查Session或Token),防止越权访问。CORS不是身份验证或授权机制,它只是一个“通道”开关。
  3. 避免信息泄露 :检查通过CORS暴露的响应头( Access-Control-Expose-Headers )是否包含敏感信息,如 Server 内部版本、 X-Powered-By 等。

5.4 合理设置 Access-Control-Max-Age

对于频繁发生的非简单请求,设置一个合理的 Access-Control-Max-Age (例如1800秒,即30分钟)可以显著减少预检请求的数量,提升性能。但也不宜设置过长,以免在策略变更时,浏览器因缓存旧策略而出现访问问题。

6. 常见问题排查与调试技巧实录

即使按照指南配置,在实际开发中依然会遇到各种奇怪的CORS问题。下面是我在多年支持项目中总结的排查清单和调试技巧。

6.1 问题排查速查表

现象 可能原因 解决方案
控制台报错: No ‘Access-Control-Allow-Origin‘ header 1. 服务器未配置任何CORS响应头。
2. 配置的Filter或Interceptor路径未覆盖当前请求URL。
3. 服务器内部发生错误(5xx),未走到设置CORS头的代码。
1. 检查服务器端CORS配置是否生效。
2. 检查请求URL是否匹配配置的路径模式。
3. 查看服务器日志,确认业务代码或过滤器链中是否有异常导致请求提前返回。
控制台报错: Response to preflight request doesn‘t pass... 1. 服务器未正确处理 OPTIONS 预检请求(返回了404或405)。
2. 预检请求的响应中, Access-Control-Allow-Methods Access-Control-Allow-Headers 不包含实际请求使用的方法或头。
1. 确保你的CORS配置能处理 OPTIONS 方法。在Spring中,全局配置或 CorsFilter 会自动处理。
2. 仔细比对浏览器预检请求的 Access-Control-Request-Method Access-Control-Request-Headers 与服务器响应的允许值是否完全匹配( 大小写敏感 )。
控制台报错: The value of the ‘Access-Control-Allow-Origin‘ header... must not be the wildcard ‘*‘ 前端请求设置了 withCredentials: true (如Axios的 axios.defaults.withCredentials = true ),但服务器响应头 Access-Control-Allow-Origin * 将服务器配置中的 Access-Control-Allow-Origin 改为明确的前端源地址,并与 Access-Control-Allow-Credentials: true 配合使用。
前端能收到响应,但JavaScript读不到自定义响应头 服务器未在 Access-Control-Expose-Headers 响应头中暴露该自定义头。 在服务器CORS配置中添加 exposedHeaders ,包含你需要让前端访问的头部名称。
使用了Spring Security后CORS失效 Spring Security的过滤器链可能在CORS过滤器之前执行,并因为认证失败而返回了403/401,导致CORS头未被添加。 1. 确保 CorsFilter 的优先级高于Spring Security的过滤器。可以通过 @Order 注解或Bean定义顺序控制。
2. 推荐 :在Spring Security配置中显式启用CORS( .cors() )并配置 CorsConfigurationSource ,如上文3.3节所示。

6.2 浏览器开发者工具调试指南

浏览器开发者工具(F12)的Network标签是调试CORS问题最强大的武器。

  1. 找到出错的请求 :红色标记的请求通常就是被CORS策略阻止的请求。
  2. 查看请求详情 :点击该请求,在 Headers 标签页中:
    • 查看 Request Headers 里的 Origin ,确认来源。
    • 对于非简单请求,查看是否有 Access-Control-Request-Method Access-Control-Request-Headers ,确认这是预检请求。
  3. 查看响应详情 :重点查看 Response Headers
    • 是否有 Access-Control-Allow-Origin ?值是否匹配请求的 Origin
    • 对于预检请求,是否有 Access-Control-Allow-Methods Access-Control-Allow-Headers ?是否包含了实际请求所需的方法和头?
    • 如果需要凭证,是否有 Access-Control-Allow-Credentials: true
  4. 对比与验证 :将浏览器的请求头与服务器的响应头逐条对比,不一致的地方就是问题所在。

6.3 服务器端日志排查

如果浏览器端信息不全,或者怀疑请求根本没到你的应用层,就需要查看服务器日志。

  • 检查请求是否到达 :在CORS Filter或拦截器中打印日志,确认OPTIONS和实际请求是否被正确处理。
  • 检查异常堆栈 :查看是否有其他过滤器或业务逻辑抛出异常,导致请求在设置CORS头之前就返回了错误响应。
  • 使用工具测试 :用Postman或curl直接发送请求(不带Origin头),可以绕过浏览器CORS检查,直接测试API本身是否工作正常,从而隔离问题是出在API功能还是CORS配置上。

最后,记住一个核心原则:CORS是浏览器的安全策略。如果你的移动端App或桌面客户端直接调用API,是不会触发CORS限制的。这有助于你在排查时确定问题的边界。配置CORS就像为你的API开一扇可控的门,既要方便合法的访问者进出,又要牢牢锁住非法的闯入者。理解机制、细致配置、严格白名单,是做好这件事的关键。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐