Skip to content

跨域处理(CORS)

✨ 基于 Spring CORS Filter 的开箱即用跨域解决方案,一个注解搞定全局跨域配置。

  • 🎯 注解驱动:通过 @EnableDefaultGlobalCors 一键启用全局跨域
  • 🔧 默认配置:内置安全合理的 CORS 默认配置
  • 🚀 最高优先级:HIGHEST_PRECEDENCE 确保 CORS 最先执行
  • 🏭 工厂模式:采用 FactoryBean 模式管理 Filter 生命周期
  • 🛡️ 安全性:支持凭证传递、预检请求缓存等安全特性
  • ⚙️ 易扩展:支持自定义 CORS 配置覆盖默认行为

快速开始

启用跨域处理

在 Spring Boot 启动类上添加 @EnableDefaultGlobalCors 注解即可启用全局跨域处理:

java
import host.springboot.framework.autoconfigure.filter.annotation.EnableDefaultGlobalCors;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableDefaultGlobalCors  // 启用全局跨域处理
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

验证效果

启动应用后,所有接口都会自动添加 CORS 响应头:

bash
# 发送跨域请求
curl -X GET http://localhost:8080/api/user/list \
  -H "Origin: http://example.com" \
  -v

# 响应头中会包含:
# Access-Control-Allow-Origin: http://example.com
# Access-Control-Allow-Credentials: true
# Access-Control-Max-Age: 3600

预检请求(OPTIONS)

bash
# 发送预检请求
curl -X OPTIONS http://localhost:8080/api/user/create \
  -H "Origin: http://example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type,Authorization" \
  -v

# 响应头中会包含:
# Access-Control-Allow-Origin: http://example.com
# Access-Control-Allow-Methods: GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,TRACE
# Access-Control-Allow-Headers: Content-Type,Authorization
# Access-Control-Max-Age: 3600

核心架构

工作流程

HTTP 请求(带 Origin 头)

DefaultCorsFilter(最高优先级)

检查是否为跨域请求

预检请求(OPTIONS)?
    ├─ 是 → 返回 CORS 响应头 + 200
    │      (Access-Control-Allow-*)

    └─ 否 → 实际请求

       添加 CORS 响应头

       继续执行后续过滤器

       执行 Controller 方法

       返回响应(包含 CORS 头)

核心类说明

类名职责说明
DefaultCorsFilter默认跨域过滤器继承 Spring 的 CorsFilter,实现 FactoryBeanPriorityOrdered
ConfigurationSourceCORS 配置源管理默认的 CORS 配置策略
@EnableDefaultGlobalCors启用注解导入 DefaultCorsFilter 到 Spring 容器

执行优先级

Filter 执行顺序(Order 数字越小越先执行):
  1. DefaultCorsFilter (HIGHEST_PRECEDENCE = Integer.MIN_VALUE)

  2. RequestBodyFilter (ORDER = -10000)

  3. XssFilter (ORDER = -9000)

  4. 其他过滤器...

  5. Controller 方法执行

💡 为什么是最高优先级?

  • CORS 预检请求需要尽早响应,避免进入业务逻辑
  • 确保所有请求都能正确添加 CORS 响应头
  • 防止其他过滤器拦截预检请求

默认 CORS 配置

配置详情

框架提供的默认 CORS 配置如下:

配置项默认值说明
allowedOriginPattern*允许所有域名访问
allowedMethodsGET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE支持所有常见 HTTP 方法
allowedHeaders*允许所有请求头
allowCredentialstrue允许携带 Cookie 和认证凭证
maxAge3600 秒(1小时)预检请求结果的缓存时间
mappings/**应用到所有路径

源码分析

java
public static CorsConfiguration defaultCorsConfiguration() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    
    // 1. 允许的域(使用 Pattern 支持更灵活的匹配)
    corsConfiguration.addAllowedOriginPattern("*");
    
    // 2. 允许的请求方式(支持所有常见 HTTP 方法)
    corsConfiguration.addAllowedMethod(HttpMethod.GET);
    corsConfiguration.addAllowedMethod(HttpMethod.HEAD);
    corsConfiguration.addAllowedMethod(HttpMethod.POST);
    corsConfiguration.addAllowedMethod(HttpMethod.PUT);
    corsConfiguration.addAllowedMethod(HttpMethod.PATCH);
    corsConfiguration.addAllowedMethod(HttpMethod.DELETE);
    corsConfiguration.addAllowedMethod(HttpMethod.OPTIONS);
    corsConfiguration.addAllowedMethod(HttpMethod.TRACE);
    
    // 3. 允许的头信息(所有请求头)
    corsConfiguration.addAllowedHeader("*");
    
    // 4. 预检请求的有效期(1小时)
    corsConfiguration.setMaxAge(Duration.ofHours(1));
    
    // 5. 是否支持安全证书(Cookie、Authorization 等)
    corsConfiguration.setAllowCredentials(true);
    
    return corsConfiguration;
}

配置说明

1. allowedOriginPattern vs allowedOrigin

java
// ❌ 旧方式:addAllowedOrigin("*")
// 当 allowCredentials=true 时,浏览器会拒绝 allowedOrigin="*"

// ✅ 新方式:addAllowedOriginPattern("*")
// 支持通配符模式,与 allowCredentials=true 兼容
corsConfiguration.addAllowedOriginPattern("*");

Pattern 支持的格式

  • * - 匹配所有域名
  • http://localhost:* - 匹配所有本地端口
  • https://*.example.com - 匹配所有 example.com 的子域名

2. allowCredentials 详解

java
corsConfiguration.setAllowCredentials(true);

作用

  • 允许前端携带 Cookie
  • 允许前端携带 Authorization 等认证头
  • 响应头包含:Access-Control-Allow-Credentials: true

前端配置(必须配合使用):

javascript
// Fetch API
fetch('http://api.example.com/user/info', {
    credentials: 'include'  // 必须设置,否则不会发送 Cookie
});

// Axios
axios.defaults.withCredentials = true;

3. maxAge 预检缓存

java
corsConfiguration.setMaxAge(Duration.ofHours(1));

作用

  • 缓存预检请求(OPTIONS)的结果 1 小时
  • 减少重复的预检请求,提升性能
  • 响应头包含:Access-Control-Max-Age: 3600

工作原理

第一次跨域请求:
  1. 浏览器发送 OPTIONS 预检请求
  2. 服务器响应 CORS 头 + Max-Age: 3600
  3. 浏览器缓存预检结果 1 小时

1 小时内的后续请求:
  - 浏览器直接发送实际请求,无需预检
  - 节省一次网络往返

1 小时后:
  - 缓存过期,重新发送预检请求

自定义 CORS 配置

方式1:完全自定义 CorsFilter

如果默认配置不满足需求,可以自定义 CorsFilter Bean:

java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.time.Duration;
import java.util.Arrays;

@Configuration
public class CustomCorsConfiguration {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        
        // 1. 仅允许特定域名
        config.addAllowedOriginPattern("https://*.example.com");
        config.addAllowedOriginPattern("http://localhost:*");
        
        // 2. 仅允许特定 HTTP 方法
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        
        // 3. 仅允许特定请求头
        config.setAllowedHeaders(Arrays.asList(
            "Content-Type",
            "Authorization",
            "X-Requested-With"
        ));
        
        // 4. 暴露特定响应头给前端
        config.setExposedHeaders(Arrays.asList(
            "X-Total-Count",
            "X-Page-Number"
        ));
        
        // 5. 允许携带凭证
        config.setAllowCredentials(true);
        
        // 6. 预检缓存 2 小时
        config.setMaxAge(Duration.ofHours(2));
        
        // 应用到所有路径
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return new CorsFilter(source);
    }
}

方式2:针对不同路径配置不同策略

java
@Configuration
public class MultiPathCorsConfiguration {
    
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        
        // 公开 API:宽松配置
        CorsConfiguration publicConfig = new CorsConfiguration();
        publicConfig.addAllowedOriginPattern("*");
        publicConfig.addAllowedMethod("*");
        publicConfig.addAllowedHeader("*");
        publicConfig.setAllowCredentials(false);  // 公开 API 不需要凭证
        source.registerCorsConfiguration("/api/public/**", publicConfig);
        
        // 管理后台 API:严格配置
        CorsConfiguration adminConfig = new CorsConfiguration();
        adminConfig.setAllowedOrigins(Arrays.asList("https://admin.example.com"));
        adminConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        adminConfig.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization"));
        adminConfig.setAllowCredentials(true);
        source.registerCorsConfiguration("/api/admin/**", adminConfig);
        
        // 用户 API:中等配置
        CorsConfiguration userConfig = new CorsConfiguration();
        userConfig.addAllowedOriginPattern("https://*.example.com");
        userConfig.addAllowedOriginPattern("http://localhost:*");
        userConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT"));
        userConfig.addAllowedHeader("*");
        userConfig.setAllowCredentials(true);
        userConfig.setMaxAge(Duration.ofHours(1));
        source.registerCorsConfiguration("/api/user/**", userConfig);
        
        return new CorsFilter(source);
    }
}

方式3:使用 WebMvcConfigurer

java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

⚠️ 注意:如果自定义了 CorsFilter Bean,框架的 @EnableDefaultGlobalCors 将不会生效。

常见场景

场景1:前后端分离开发

需求:前端本地开发(http://localhost:3000),后端本地运行(http://localhost:8080

java
// 方式1:使用默认配置(推荐)
@SpringBootApplication
@EnableDefaultGlobalCors
public class Application {
    // 默认配置已支持所有域名,包括 localhost
}

// 方式2:自定义配置
@Configuration
public class DevCorsConfig {
    
    @Bean
    @Profile("dev")  // 仅开发环境启用
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("http://localhost:*");
        config.addAllowedOriginPattern("http://127.0.0.1:*");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

场景2:生产环境严格配置

需求:仅允许特定域名访问,防止跨域攻击

java
@Configuration
public class ProdCorsConfig {
    
    @Bean
    @Profile("prod")  // 仅生产环境启用
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        
        // 仅允许官网域名
        config.setAllowedOrigins(Arrays.asList(
            "https://www.example.com",
            "https://app.example.com",
            "https://admin.example.com"
        ));
        
        // 仅允许安全的 HTTP 方法
        config.setAllowedMethods(Arrays.asList(
            "GET", "POST", "PUT", "DELETE"
        ));
        
        // 仅允许必要的请求头
        config.setAllowedHeaders(Arrays.asList(
            "Content-Type",
            "Authorization",
            "X-Requested-With"
        ));
        
        config.setAllowCredentials(true);
        config.setMaxAge(Duration.ofHours(2));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

场景3:特定接口禁用 CORS

需求:某些内部 API 不需要 CORS 支持

java
@Configuration
public class SelectiveCorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        
        // 公开 API 启用 CORS
        CorsConfiguration publicConfig = new CorsConfiguration();
        publicConfig.addAllowedOriginPattern("*");
        publicConfig.addAllowedMethod("*");
        publicConfig.addAllowedHeader("*");
        publicConfig.setAllowCredentials(true);
        source.registerCorsConfiguration("/api/public/**", publicConfig);
        
        // 内部 API 不配置 CORS(浏览器同源策略会阻止跨域访问)
        // source.registerCorsConfiguration("/api/internal/**", null);
        
        return new CorsFilter(source);
    }
}

场景4:处理复杂的预检请求

需求:前端使用自定义请求头,需要在预检请求中声明

java
@Configuration
public class CustomHeaderCorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        
        config.addAllowedOriginPattern("*");
        config.addAllowedMethod("*");
        
        // 允许自定义请求头
        config.setAllowedHeaders(Arrays.asList(
            "Content-Type",
            "Authorization",
            "X-Requested-With",
            "X-Custom-Token",      // 自定义:业务 Token
            "X-Device-Id",         // 自定义:设备 ID
            "X-App-Version"        // 自定义:应用版本
        ));
        
        // 暴露自定义响应头给前端
        config.setExposedHeaders(Arrays.asList(
            "X-Total-Count",       // 分页总数
            "X-Rate-Limit",        // 限流剩余次数
            "X-Request-Id"         // 请求追踪 ID
        ));
        
        config.setAllowCredentials(true);
        config.setMaxAge(Duration.ofHours(1));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

前端对应配置(Axios):

javascript
axios.defaults.withCredentials = true;

// 发送自定义请求头
axios.get('/api/user/info', {
    headers: {
        'X-Custom-Token': 'your-token',
        'X-Device-Id': 'device-123',
        'X-App-Version': '1.0.0'
    }
}).then(response => {
    // 读取自定义响应头
    const totalCount = response.headers['x-total-count'];
    const rateLimit = response.headers['x-rate-limit'];
    const requestId = response.headers['x-request-id'];
});

场景5:微服务架构下的 CORS

需求:多个微服务共享统一的 CORS 配置

java
// 方式1:在网关统一处理 CORS
@Configuration
public class GatewayCorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("*");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        config.setMaxAge(Duration.ofHours(1));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

// 方式2:每个微服务使用框架默认配置
@SpringBootApplication
@EnableDefaultGlobalCors
public class UserServiceApplication {
    // 使用默认配置,简单快捷
}

最佳实践

✅ 推荐做法

java
// 1. 开发环境使用宽松配置,生产环境使用严格配置
@Configuration
public class EnvironmentAwareCorsConfig {
    
    @Bean
    public CorsFilter corsFilter(@Value("${spring.profiles.active}") String profile) {
        CorsConfiguration config = new CorsConfiguration();
        
        if ("dev".equals(profile) || "test".equals(profile)) {
            // 开发/测试:宽松配置
            config.addAllowedOriginPattern("*");
            config.addAllowedMethod("*");
            config.addAllowedHeader("*");
        } else {
            // 生产:严格配置
            config.setAllowedOrigins(Arrays.asList(
                "https://www.example.com",
                "https://app.example.com"
            ));
            config.setAllowedMethods(Arrays.asList(
                "GET", "POST", "PUT", "DELETE"
            ));
            config.setAllowedHeaders(Arrays.asList(
                "Content-Type", "Authorization"
            ));
        }
        
        config.setAllowCredentials(true);
        config.setMaxAge(Duration.ofHours(1));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

// 2. 使用 allowedOriginPattern 而不是 allowedOrigin
// ✅ 推荐
config.addAllowedOriginPattern("https://*.example.com");

// ❌ 不推荐(当 allowCredentials=true 时会失败)
config.addAllowedOrigin("*");

// 3. 合理设置预检缓存时间
config.setMaxAge(Duration.ofHours(1));  // 1小时,平衡性能和灵活性

// 4. 暴露必要的响应头给前端
config.setExposedHeaders(Arrays.asList(
    "X-Total-Count",
    "X-Request-Id"
));

// 5. 使用 Profile 区分环境
@Bean
@Profile("dev")
public CorsFilter devCorsFilter() {
    // 开发环境配置
}

@Bean
@Profile("prod")
public CorsFilter prodCorsFilter() {
    // 生产环境配置
}

❌ 不推荐做法

java
// ❌ 1. 生产环境使用 allowedOrigin("*") + allowCredentials(true)
config.addAllowedOrigin("*");  // 浏览器会拒绝此配置
config.setAllowCredentials(true);

// ✅ 正确做法
config.addAllowedOriginPattern("*");
config.setAllowCredentials(true);

// ❌ 2. 忘记设置 allowCredentials
config.addAllowedOriginPattern("*");
// 前端无法发送 Cookie 和 Authorization

// ✅ 正确做法
config.setAllowCredentials(true);

// ❌ 3. 预检缓存时间过长
config.setMaxAge(Duration.ofDays(30));  // 太长,配置更新需要 30 天才能生效

// ✅ 正确做法
config.setMaxAge(Duration.ofHours(1));  // 1小时足够

// ❌ 4. 在 Controller 上使用 @CrossOrigin(分散配置,难以维护)
@RestController
@CrossOrigin(origins = "*")  // 不推荐
public class UserController {
    // ...
}

// ✅ 正确做法:使用全局 CorsFilter
@SpringBootApplication
@EnableDefaultGlobalCors
public class Application {
    // ...
}

// ❌ 5. 忘记在前端设置 withCredentials
// JavaScript
fetch('/api/user/info');  // 不会发送 Cookie

// ✅ 正确做法
fetch('/api/user/info', {
    credentials: 'include'  // 发送 Cookie
});

安全性考虑

1. 生产环境严格配置

java
@Configuration
@Profile("prod")
public class SecureCorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        
        // ⚠️ 严格限制允许的域名
        config.setAllowedOrigins(Arrays.asList(
            "https://www.example.com",
            "https://app.example.com"
        ));
        // 不要使用 config.addAllowedOriginPattern("*");
        
        // ⚠️ 仅允许必要的 HTTP 方法
        config.setAllowedMethods(Arrays.asList(
            "GET", "POST", "PUT", "DELETE"
        ));
        // 不要使用 config.addAllowedMethod("*");
        
        // ⚠️ 仅允许必要的请求头
        config.setAllowedHeaders(Arrays.asList(
            "Content-Type",
            "Authorization"
        ));
        // 不要使用 config.addAllowedHeader("*");
        
        config.setAllowCredentials(true);
        config.setMaxAge(Duration.ofHours(1));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

2. 动态域名白名单

java
@Configuration
public class DynamicCorsConfig {
    
    @Value("${cors.allowed.origins}")
    private List<String> allowedOrigins;
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        
        // 从配置文件读取允许的域名
        config.setAllowedOrigins(allowedOrigins);
        
        config.setAllowedMethods(Arrays.asList(
            "GET", "POST", "PUT", "DELETE"
        ));
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        config.setMaxAge(Duration.ofHours(1));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

配置文件(application.yml):

yaml
# 开发环境
spring:
  profiles:
    active: dev

cors:
  allowed:
    origins:
      - http://localhost:3000
      - http://localhost:8080
      - http://127.0.0.1:3000

---
# 生产环境
spring:
  config:
    activate:
      on-profile: prod

cors:
  allowed:
    origins:
      - https://www.example.com
      - https://app.example.com
      - https://admin.example.com

3. CSRF 防护

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // CORS 配置
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // CSRF 配置
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .ignoringRequestMatchers("/api/public/**")  // 公开 API 不需要 CSRF
            )
            
            // 其他安全配置...
            ;
        
        return http.build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("https://www.example.com"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

常见问题

Q: 为什么启用了 CORS 但前端还是报错?

A: 常见原因及解决方案:

1. 前端未设置 withCredentials

javascript
// ❌ 错误
fetch('/api/user/info');

// ✅ 正确
fetch('/api/user/info', {
    credentials: 'include'
});

// Axios
axios.defaults.withCredentials = true;

2. allowedOrigin 和 allowCredentials 冲突

java
// ❌ 错误
config.addAllowedOrigin("*");  // 使用 allowedOrigin("*")
config.setAllowCredentials(true);  // 浏览器会拒绝

// ✅ 正确
config.addAllowedOriginPattern("*");  // 使用 allowedOriginPattern("*")
config.setAllowCredentials(true);

3. 多个 CorsFilter Bean 冲突

java
// 检查是否定义了多个 CorsFilter
// 只保留一个即可
@Bean
public CorsFilter corsFilter() {
    // 自定义配置
}

4. 预检请求被其他过滤器拦截

java
// 确保 CorsFilter 优先级最高
public class CustomCorsFilter extends CorsFilter implements Ordered {
    
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;  // 最高优先级
    }
}

Q: 如何调试 CORS 问题?

A: 使用浏览器开发者工具检查:

1. 检查预检请求(OPTIONS)

bash
# Network 面板筛选 OPTIONS 请求
# 查看请求头:
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

# 查看响应头(应该包含):
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600

2. 检查实际请求

bash
# 查看请求头:
Origin: http://localhost:3000

# 查看响应头(应该包含):
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true

3. 常见错误信息

❌ "Access to fetch at 'xxx' from origin 'xxx' has been blocked by CORS policy: 
     No 'Access-Control-Allow-Origin' header is present on the requested resource."
→ 解决:检查是否启用了 CORS 配置

❌ "Access to fetch at 'xxx' from origin 'xxx' has been blocked by CORS policy: 
     The value of the 'Access-Control-Allow-Origin' header in the response must not 
     be the wildcard '*' when the request's credentials mode is 'include'."
→ 解决:使用 allowedOriginPattern("*") 替代 allowedOrigin("*")

❌ "Access to fetch at 'xxx' from origin 'xxx' has been blocked by CORS policy: 
     Response to preflight request doesn't pass access control check: 
     The value of the 'Access-Control-Allow-Credentials' header in the response is '' 
     which must be 'true' when the request's credentials mode is 'include'."
→ 解决:设置 config.setAllowCredentials(true)

Q: @EnableDefaultGlobalCors 和自定义 CorsFilter 有什么区别?

A: 对比如下:

特性@EnableDefaultGlobalCors自定义 CorsFilter
使用难度简单(一个注解)需要编写配置类
配置灵活性固定(默认配置)完全自定义
适用场景开发环境、快速启动生产环境、精细控制
优先级HIGHEST_PRECEDENCE可自定义
多路径配置不支持支持

选择建议

  • 开发环境:使用 @EnableDefaultGlobalCors,快速启动
  • 生产环境:自定义 CorsFilter,严格控制

Q: 如何在不同环境使用不同的 CORS 配置?

A: 使用 @Profile 注解:

java
// 开发环境:宽松配置
@Configuration
@Profile({"dev", "test"})
public class DevCorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("*");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

// 生产环境:严格配置
@Configuration
@Profile("prod")
public class ProdCorsConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("https://www.example.com"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization"));
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

Q: 如何禁用 CORS?

A: 有两种方式:

方式1:不添加 @EnableDefaultGlobalCors 注解

java
@SpringBootApplication
// 不添加 @EnableDefaultGlobalCors
public class Application {
    // CORS 不会启用
}

方式2:不定义 CorsFilter Bean

java
// 删除或注释掉 CorsFilter Bean
// @Bean
// public CorsFilter corsFilter() {
//     ...
// }

Q: 预检请求(OPTIONS)过多影响性能怎么办?

A: 通过增加 maxAge 缓存预检结果:

java
config.setMaxAge(Duration.ofHours(2));  // 缓存 2 小时

// 浏览器会缓存预检结果,2 小时内的相同请求不再发送 OPTIONS

建议值

  • 开发环境:Duration.ofMinutes(30) - 30 分钟,便于调试
  • 生产环境:Duration.ofHours(1) - 1 小时,平衡性能和灵活性

Q: 如何允许前端读取自定义响应头?

A: 使用 setExposedHeaders() 暴露响应头:

java
config.setExposedHeaders(Arrays.asList(
    "X-Total-Count",      // 分页总数
    "X-Request-Id",       // 请求追踪 ID
    "X-Rate-Limit",       // 限流剩余次数
    "X-Custom-Header"     // 自定义响应头
));

前端读取

javascript
fetch('/api/user/list')
    .then(response => {
        const totalCount = response.headers.get('X-Total-Count');
        const requestId = response.headers.get('X-Request-Id');
        console.log('Total:', totalCount, 'RequestId:', requestId);
        return response.json();
    });

💡 提示

  • CORS 是浏览器的安全机制,不影响服务端到服务端的调用
  • 预检请求(OPTIONS)是浏览器自动发送的,开发者无需手动处理
  • allowedOriginPattern 支持通配符,比 allowedOrigin 更灵活
  • 生产环境务必使用严格的域名白名单,避免安全风险

Released under the Apache-2.0 License