摘要

CORS(跨域资源共享)是Web开发中几乎每个Java后端开发者都会遇到的“拦路虎”。当你在本地运行前端项目调用接口时,突然看到浏览器控制台报出熟悉的红色错误——No 'Access-Control-Allow-Origin' header is present——这就是CORS问题在“抗议”了。本文将从跨域问题的本质出发,系统讲解CORS的工作原理,然后深入Spring Boot、Spring Security、JAX-RS等主流Java框架中的解决方案,最后讨论生产环境下的最佳实践和常见踩坑点。

1. 为什么要跨域?同源策略的本质

1.1 什么是同源策略?

在理解跨域之前,先要理解同源策略(Same-Origin Policy)——这是浏览器最重要的安全机制之一。

同源的定义:两个URL的协议、域名、端口三者完全一致,才算同源。

URL A URL B 是否同源 原因
https://example.com:443/page https://example.com:443/api ✅ 是 协议、域名、端口一致
http://example.com/page https://example.com/api ❌ 否 协议不同(http vs https)
https://example.com/page https://api.example.com/api ❌ 否 域名不同(子域名不同)
https://example.com:443/page https://example.com:8080/api ❌ 否 端口不同

同源策略的限制:非同源的请求会被浏览器拦截,主要限制三类行为:

  • DOM访问限制:无法读取跨域页面的DOM

  • Cookie/Cache限制:无法共享跨域的Cookie、LocalStorage

  • 网络请求限制:AJAX/Fetch请求无法获取跨域响应(这是本文重点关注的内容)

1.2 跨域不等于安全漏洞

一个常见的误解是:“浏览器为什么要阻止跨域?是不是跨域就是错误的?”

恰恰相反。如果没有同源策略,恶意网站evil.com就能轻松访问你在bank.com上的会话Cookie,从而伪造你发起转账请求。同源策略保护的是用户在不同网站之间的身份隔离

但在前后端分离的架构下,前端(http://localhost:3000)需要调用后端API(http://localhost:8080),跨域请求是刚需。这就引出了CORS——一个受控的、安全的跨域共享机制

1.3 错误信息长什么样?

当跨域请求被拦截时,浏览器控制台会输出类似信息:

text

Access to XMLHttpRequest at 'http://localhost:8080/api/user' from origin 
'http://localhost:3000' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

注意:请求实际上已经到达后端服务器并且执行了(比如数据库已经写入),只是浏览器拦截了响应。这是一个常见误区——后端认为一切正常,前端却报错。

2. CORS工作原理:浏览器与服务器的握手协议

CORS(Cross-Origin Resource Sharing,跨域资源共享)通过HTTP头来协商跨域请求的合法性,整个过程由浏览器自动发起,不需要前端代码额外处理。

2.1 简单请求 vs 预检请求

CORS将请求分为两类:

简单请求(Simple Request)

同时满足以下条件:

  • 方法为GETHEADPOST之一

  • 仅包含CORS安全的头:AcceptAccept-LanguageContent-LanguageContent-Type(仅限application/x-www-form-urlencodedmultipart/form-datatext/plain

简单请求的流程:浏览器直接发送请求,响应中必须包含Access-Control-Allow-Origin,否则浏览器拦截。

text

浏览器 ── GET /api/data (Origin: http://frontend.com) ──→ 服务器
浏览器 ←─ 200 OK (Access-Control-Allow-Origin: http://frontend.com) ── 服务器

预检请求(Preflight Request)

不满足简单请求条件的,会先发送一次OPTIONS请求“探路”:

  • 方法为PUTDELETEPATCH

  • 使用自定义头(如AuthorizationX-Requested-With

  • Content-Typeapplication/json

流程:

text

浏览器 ── OPTIONS /api/data (Origin, Access-Control-Request-Method) ──→ 服务器
浏览器 ←─ 204/200 (Access-Control-Allow-*) ── 服务器(预检通过)
浏览器 ── PUT /api/data (真实的请求) ──→ 服务器
浏览器 ←─ 响应 ── 服务器

2.2 核心响应头解析

响应头 作用 示例
Access-Control-Allow-Origin 允许哪些源访问 * 或 https://frontend.com
Access-Control-Allow-Methods 允许哪些HTTP方法 GET, POST, PUT, DELETE
Access-Control-Allow-Headers 允许哪些自定义头 Authorization, Content-Type
Access-Control-Expose-Headers 允许前端读取哪些响应头 X-Total-Count
Access-Control-Allow-Credentials 是否允许携带Cookie/凭证 true
Access-Control-Max-Age 预检结果缓存时间(秒) 3600

2.3 带凭证的请求

如果前端需要发送Cookie或HTTP认证信息,需要:

  • 前端设置:fetch(url, { credentials: 'include' }) 或 xhr.withCredentials = true

  • 后端响应头必须:Access-Control-Allow-Origin不能为*,且必须明确设置Access-Control-Allow-Credentials: true

3. Java解决方案实战

3.1 Spring Boot:最优雅的方式

方式一:全局配置(推荐)

编写一个配置类,统一管理CORS策略:

java

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")                    // 所有路径
                .allowedOriginPatterns(               // 允许的源(支持通配符)
                    "http://localhost:3000",
                    "https://frontend.com"
                )
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")                   // 允许所有请求头
                .allowCredentials(true)                // 允许携带Cookie
                .maxAge(3600);                         // 预检缓存1小时
    }
}

方式二:使用@CrossOrigin注解(局部配置)

在Controller或方法上直接添加注解:

java

@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")
public class UserController {

    @GetMapping("/user")
    @CrossOrigin(originPatterns = "https://*.example.com")  // 方法级别覆盖
    public User getUser() {
        return new User("张三");
    }
}

方式三:使用CorsFilter(Filter级别)

精细控制,适合需要在Filter链条中提前处理的场景:

java

@Bean
public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.setAllowedOriginPatterns(Arrays.asList("http://localhost:3000"));
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

3.2 Spring Security整合

如果项目中使用了Spring Security,CORS配置必须放在Security Filter之前:

java

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))  // 启用CORS
            .csrf(csrf -> csrf.disable())  // 跨域请求通常需要关闭CSRF(或用Token代替)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(Arrays.asList("http://localhost:3000"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

⚠️ 注意:启用allowCredentials(true)后,Spring Security的CSRF保护可能会拦截跨域请求。通常的做法是:对无状态的REST API关闭CSRF,或使用JWT等Token机制进行认证。

3.3 JAX-RS / Jersey

使用Jersey框架时,通过过滤器实现:

java

@Provider
public class CorsFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext,
                       ContainerResponseContext responseContext) {
        responseContext.getHeaders().add(
            "Access-Control-Allow-Origin", "http://localhost:3000");
        responseContext.getHeaders().add(
            "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        responseContext.getHeaders().add(
            "Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization");
        responseContext.getHeaders().add(
            "Access-Control-Allow-Credentials", "true");
        
        // 处理预检请求
        if ("OPTIONS".equalsIgnoreCase(requestContext.getMethod())) {
            responseContext.setStatus(Status.OK.getStatusCode());
        }
    }
}

3.4 Spring Cloud Gateway(网关层统一处理)

在微服务架构中,通常在网关层统一处理CORS:

yaml

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns:
              - "http://localhost:3000"
            allowed-methods: "*"
            allowed-headers: "*"
            allow-credentials: true
            max-age: 3600

4. 生产环境最佳实践

4.1 不要使用*(除非绝对必要)

java

// ❌ 不推荐:生产环境不要用*
.allowedOrigins("*")

// ✅ 推荐:明确指定允许的源
.allowedOrigins("https://frontend-prod.com", "https://admin.example.com")

// ✅ 或者使用模式匹配
.allowedOriginPatterns("https://*.myapp.com")

4.2 环境差异化配置

开发环境、测试环境、生产环境的CORS策略应该不同:

java

@Configuration
public class CorsConfig {

    @Value("${cors.allowed.origins}")
    private String[] allowedOrigins;
    
    @Value("${cors.allow-credentials:false}")
    private boolean allowCredentials;

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                        .allowedOrigins(allowedOrigins)
                        .allowedMethods("GET", "POST", "PUT", "DELETE")
                        .allowCredentials(allowCredentials);
            }
        };
    }
}

application-dev.yml:

yaml

cors:
  allowed-origins: "http://localhost:3000,http://localhost:8080"
  allow-credentials: true

application-prod.yml:

yaml

cors:
  allowed-origins: "https://app.example.com"
  allow-credentials: true

4.3 处理OPTIONS请求的性能优化

预检请求会增加一次网络往返。合理设置maxAge可以减少预检请求次数:

java

.maxAge(7200)  // 缓存2小时,单位:秒

4.4 安全考量

  • 不要暴露内部域名:CORS配置中不要出现localhost或内部IP的生产配置

  • 谨防Credentials泄露allowCredentials(true)时,allowedOrigins不能为*

  • 最小权限原则:只开放必要的AllowedMethodsAllowedHeaders

5. 常见问题排查指南

问题1:配置了CORS仍然报错

排查步骤

  1. 打开浏览器开发者工具 → Network,查看OPTIONS预检请求的响应头

  2. 确认后端确实返回了正确的Access-Control-Allow-Origin

  3. 检查是否有其他过滤器/拦截器覆盖了CORS头

java

// 常见错误:项目中的自定义过滤器在CORS之前返回了响应
// 解决:确保CorsFilter是第一个执行的过滤器
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
    // ...
}

问题2:预检请求(OPTIONS)返回403

  • Spring Security会拦截OPTIONS请求,需要在Security配置中放行:

java

http.authorizeHttpRequests(auth -> auth
    .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
    // ... 其他规则
);

问题3:携带Cookie失败

前端和后端都需要配置:

  • 前端:credentials: 'include' 或 withCredentials: true

  • 后端:allowCredentials(true) + allowedOrigins不能为*

问题4:Nginx反向代理后的CORS

如果后端前面有Nginx,可以配置Nginx直接返回CORS头,减轻后端压力:

nginx

location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://frontend.com' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
    
    if ($request_method = 'OPTIONS') {
        return 204;
    }
    
    proxy_pass http://backend:8080;
}

6. 总结:核心要点一览

场景 推荐方案
Spring Boot单体应用 WebMvcConfigurer 全局配置
使用Spring Security 配置http.cors() + 放行OPTIONS
微服务架构 网关层统一处理(Spring Cloud Gateway)
开发环境 可使用*或宽泛模式,方便调试
生产环境 严格指定源,最小权限原则
需要携带Cookie allowCredentials(true) + 明确指定Origin
性能敏感 设置合理的maxAge

最终建议

CORS不是Bug,也不是“跨域问题”的最终答案——它是浏览器与服务器之间的一道安全闸门。理解其原理后,你会发现绝大多数CORS问题只需要一个配置类就能解决。当遇到奇怪的问题时,永远记得先用浏览器开发者工具查看预检请求的请求/响应头,95%的问题在这一步就能找到答案。

Logo

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

更多推荐