Skip to content

跨域配置

概述

在现代微服务架构中,跨域资源共享(CORS)是一个不可避免的问题。当前端应用部署在不同的域名或端口下,需要访问后端 API 时,浏览器会执行同源策略检查,阻止跨域请求。Spring Cloud Gateway 作为微服务架构中的网关组件,提供了灵活的 CORS 配置方案来解决这个问题。

CORS(Cross-Origin Resource Sharing)是一种机制,它允许 Web 页面从不同的域访问资源。Gateway 的 CORS 配置可以在网关层面统一处理跨域问题,避免在每个微服务中重复配置。

CORS 配置方式

Spring Cloud Gateway 提供了两种 CORS 配置方式:

  1. 全局 CORS 配置 - 对所有路由生效
  2. 路由级 CORS 配置 - 针对特定路由配置

全局 CORS 配置

全局 CORS 配置通过 URL 模式映射到 Spring Framework 的 CorsConfiguration,适用于需要统一处理所有跨域请求的场景。

基本配置示例

yaml
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          "[/**]":
            allowedOrigins: "https://docs.spring.io"
            allowedMethods:
              - GET

实际业务场景配置

在实际项目中,我们通常需要更灵活的配置:

kotlin
@Configuration
@EnableWebFluxSecurity
class GatewayConfig {

    /**
     * 全局 CORS 配置
     * 适用于电商系统中前端和管理后台需要访问同一套 API 的场景
     */
    @Bean
    fun corsWebFilter(): CorsWebFilter {
        val corsConfig = CorsConfiguration().apply {
            // 允许的源域名 - 生产环境应该配置具体域名
            allowedOriginPatterns = listOf("*")

            // 允许的 HTTP 方法
            allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")

            // 允许的请求头
            allowedHeaders = listOf("*")

            // 是否允许携带凭证(如 Cookie)
            allowCredentials = true

            // 预检请求的缓存时间(秒)
            maxAge = 3600L
        }

        val source = UrlBasedCorsConfigurationSource().apply {
            registerCorsConfiguration("/**", corsConfig)
        }

        return CorsWebFilter(source)
    }
}
java
@Configuration
@EnableWebFluxSecurity
public class GatewayConfig {

    /**
     * 全局 CORS 配置
     * 适用于电商系统中前端和管理后台需要访问同一套 API 的场景
     */
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration corsConfig = new CorsConfiguration();

        // 允许的源域名 - 生产环境应该配置具体域名
        corsConfig.setAllowedOriginPatterns(Arrays.asList("*"));

        // 允许的 HTTP 方法
        corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));

        // 允许的请求头
        corsConfig.setAllowedHeaders(Arrays.asList("*"));

        // 是否允许携带凭证(如 Cookie)
        corsConfig.setAllowCredentials(true);

        // 预检请求的缓存时间(秒)
        corsConfig.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig);

        return new CorsWebFilter(source);
    }
}

配置文件方式

yaml
spring:
  cloud:
    gateway:
      globalcors:
        # 处理未被路由谓词处理的简单 URL 映射
        add-to-simple-url-handler-mapping: true
        cors-configurations:
          "[/**]":
            # 允许的源域名
            allowedOrigins:
              - "https://admin.example.com"
              - "https://app.example.com"
            # 允许的 HTTP 方法
            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS
            # 允许的请求头
            allowedHeaders:
              - "*"
            # 允许携带凭证
            allowCredentials: true
            # 预检请求缓存时间
            maxAge: 3600

TIP

add-to-simple-url-handler-mapping: true 配置对于支持 CORS 预检请求非常重要,特别是当路由谓词因为 HTTP 方法是 OPTIONS 而不匹配时。

路由级 CORS 配置

路由级配置允许为特定路由应用不同的 CORS 策略,适用于不同服务需要不同跨域策略的场景。

基本路由配置

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: cors_route
          uri: https://example.org
          predicates:
            - Path=/service/**
          metadata:
            cors:
              allowedOrigins: "*"
              allowedMethods:
                - GET
                - POST
              allowedHeaders: "*"
              maxAge: 30

实际业务场景 - 多服务差异化配置

在实际项目中,不同的微服务可能需要不同的 CORS 策略:

kotlin
@Configuration
class GatewayRoutesConfig {

    /**
     * 配置不同服务的路由和 CORS 策略
     */
    @Bean
    fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            // 用户服务 - 严格的 CORS 策略
            .route("user-service") { r ->
                r.path("/api/users/**")
                    .uri("lb://user-service")
                    .metadata("cors", mapOf(
                        "allowedOrigins" to listOf("https://app.example.com"),
                        "allowedMethods" to listOf("GET", "POST", "PUT"),
                        "allowedHeaders" to listOf("Content-Type", "Authorization"),
                        "allowCredentials" to true,
                        "maxAge" to 7200
                    ))
            }
            // 公开 API - 宽松的 CORS 策略
            .route("public-api") { r ->
                r.path("/api/public/**")
                    .uri("lb://public-service")
                    .metadata("cors", mapOf(
                        "allowedOrigins" to listOf("*"),
                        "allowedMethods" to listOf("GET"),
                        "allowedHeaders" to listOf("*"),
                        "allowCredentials" to false,
                        "maxAge" to 3600
                    ))
            }
            // 管理后台 - 管理员专用
            .route("admin-api") { r ->
                r.path("/api/admin/**")
                    .uri("lb://admin-service")
                    .metadata("cors", mapOf(
                        "allowedOrigins" to listOf("https://admin.example.com"),
                        "allowedMethods" to listOf("GET", "POST", "PUT", "DELETE"),
                        "allowedHeaders" to listOf("Content-Type", "Authorization", "X-Admin-Token"),
                        "allowCredentials" to true,
                        "maxAge" to 1800
                    ))
            }
            .build()
    }
}

YAML 配置方式

yaml
spring:
  cloud:
    gateway:
      routes:
        # 用户服务路由
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          metadata:
            cors:
              allowedOrigins:
                - "https://app.example.com"
              allowedMethods:
                - GET
                - POST
                - PUT
              allowedHeaders:
                - "Content-Type"
                - "Authorization"
              allowCredentials: true
              maxAge: 7200

        # 公开 API 路由
        - id: public-api
          uri: lb://public-service
          predicates:
            - Path=/api/public/**
          metadata:
            cors:
              allowedOrigins: "*"
              allowedMethods:
                - GET
              allowedHeaders: "*"
              allowCredentials: false
              maxAge: 3600

        # 管理后台路由
        - id: admin-api
          uri: lb://admin-service
          predicates:
            - Path=/api/admin/**
          metadata:
            cors:
              allowedOrigins:
                - "https://admin.example.com"
              allowedMethods:
                - GET
                - POST
                - PUT
                - DELETE
              allowedHeaders:
                - "Content-Type"
                - "Authorization"
                - "X-Admin-Token"
              allowCredentials: true
              maxAge: 1800

如果路由中没有 `Path` 谓词,系统会自动应用 `/**` 模式。

高级配置场景

动态 CORS 配置

在某些场景下,我们需要根据环境或配置动态调整 CORS 策略:

kotlin
@Configuration
class DynamicCorsConfig {

    @Value("\${app.cors.allowed-origins:*}")
    private lateinit var allowedOrigins: List<String>

    @Value("\${app.cors.allow-credentials:true}")
    private var allowCredentials: Boolean = true

    /**
     * 根据环境动态配置 CORS
     * 开发环境:允许所有域名
     * 生产环境:只允许指定域名
     */
    @Bean
    @ConditionalOnProperty(name = ["app.cors.dynamic"], havingValue = "true")
    fun dynamicCorsWebFilter(): CorsWebFilter {
        val corsConfig = CorsConfiguration().apply {
            allowedOriginPatterns = if (allowedOrigins.contains("*")) {
                // 开发环境 - 允许所有域名
                listOf("*")
            } else {
                // 生产环境 - 指定域名
                allowedOrigins
            }

            allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
            allowedHeaders = listOf("*")
            allowCredentials = this@DynamicCorsConfig.allowCredentials
            maxAge = 3600L
        }

        val source = UrlBasedCorsConfigurationSource().apply {
            registerCorsConfiguration("/**", corsConfig)
        }

        return CorsWebFilter(source)
    }
}

自定义 CORS 过滤器

对于复杂的业务需求,可以实现自定义的 CORS 过滤器:

kotlin
@Component
class CustomCorsGlobalFilter : GlobalFilter, Ordered {

    /**
     * 自定义 CORS 处理逻辑
     * 可以根据请求头、路径等信息动态决定 CORS 策略
     */
    override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
        val request = exchange.request
        val response = exchange.response

        // 检查是否为跨域请求
        if (isCorsRequest(request)) {
            // 根据请求路径决定 CORS 策略
            val corsConfig = determineCorsConfig(request.path.value())

            // 设置 CORS 响应头
            setCorsHeaders(response, corsConfig)

            // 处理预检请求
            if (request.method == HttpMethod.OPTIONS) {
                response.statusCode = HttpStatus.OK
                return response.setComplete()
            }
        }

        return chain.filter(exchange)
    }

    /**
     * 判断是否为跨域请求
     */
    private fun isCorsRequest(request: ServerHttpRequest): Boolean {
        return request.headers.origin != null
    }

    /**
     * 根据路径确定 CORS 配置
     */
    private fun determineCorsConfig(path: String): CorsConfiguration {
        return when {
            path.startsWith("/api/admin") -> {
                // 管理接口 - 严格配置
                CorsConfiguration().apply {
                    allowedOrigins = listOf("https://admin.example.com")
                    allowedMethods = listOf("GET", "POST", "PUT", "DELETE")
                    allowCredentials = true
                }
            }
            path.startsWith("/api/public") -> {
                // 公开接口 - 宽松配置
                CorsConfiguration().apply {
                    allowedOriginPatterns = listOf("*")
                    allowedMethods = listOf("GET")
                    allowCredentials = false
                }
            }
            else -> {
                // 默认配置
                CorsConfiguration().apply {
                    allowedOriginPatterns = listOf("*")
                    allowedMethods = listOf("GET", "POST", "PUT", "DELETE")
                    allowCredentials = true
                }
            }
        }
    }

    /**
     * 设置 CORS 响应头
     */
    private fun setCorsHeaders(response: ServerHttpResponse, corsConfig: CorsConfiguration) {
        response.headers.apply {
            accessControlAllowOrigin = corsConfig.allowedOrigins?.firstOrNull() ?: "*"
            accessControlAllowMethods = corsConfig.allowedMethods?.joinToString(", ") ?: "*"
            accessControlAllowHeaders = corsConfig.allowedHeaders?.joinToString(", ") ?: "*"
            accessControlAllowCredentials = corsConfig.allowCredentials?.toString() ?: "false"
            accessControlMaxAge = corsConfig.maxAge?.toString() ?: "3600"
        }
    }

    override fun getOrder(): Int = -1 // 确保在其他过滤器之前执行
}

常见配置参数说明

参数说明示例值注意事项
allowedOrigins允许的源域名["https://example.com"]生产环境不建议使用 *
allowedOriginPatterns允许的源域名模式["https://*.example.com"]支持通配符匹配
allowedMethods允许的 HTTP 方法["GET", "POST", "PUT"]包含 OPTIONS 用于预检
allowedHeaders允许的请求头["Content-Type", "Authorization"]* 表示允许所有
exposedHeaders暴露给客户端的响应头["X-Total-Count"]客户端可访问的自定义头
allowCredentials是否允许携带凭证trueallowedOrigins: "*" 冲突
maxAge预检请求缓存时间(秒)3600减少预检请求频率

当 `allowCredentials` 为 `true` 时,不能使用 `allowedOrigins: "*"`,必须指定具体的域名或使用 `allowedOriginPatterns`。

最佳实践

1. 安全性优先

DANGER

安全提醒生产环境中,永远不要使用 allowedOrigins: "*"allowCredentials: true 的组合,这会带来严重的安全风险。

kotlin
// ❌ 不安全的配置
val unsafeCorsConfig = CorsConfiguration().apply {
    allowedOrigins = listOf("*")
    allowCredentials = true // 这种组合是危险的
}

// ✅ 安全的配置
val safeCorsConfig = CorsConfiguration().apply {
    allowedOrigins = listOf("https://app.example.com", "https://admin.example.com")
    allowCredentials = true
}

2. 环境差异化配置

kotlin
@Configuration
class EnvironmentBasedCorsConfig {

    @Bean
    @Profile("dev")
    fun devCorsConfig(): CorsWebFilter {
        // 开发环境 - 宽松配置便于调试
        val corsConfig = CorsConfiguration().apply {
            allowedOriginPatterns = listOf("*")
            allowedMethods = listOf("*")
            allowedHeaders = listOf("*")
            allowCredentials = true
            maxAge = 3600L
        }

        return createCorsWebFilter(corsConfig)
    }

    @Bean
    @Profile("prod")
    fun prodCorsConfig(): CorsWebFilter {
        // 生产环境 - 严格配置保证安全
        val corsConfig = CorsConfiguration().apply {
            allowedOrigins = listOf(
                "https://app.example.com",
                "https://mobile.example.com"
            )
            allowedMethods = listOf("GET", "POST", "PUT", "DELETE")
            allowedHeaders = listOf("Content-Type", "Authorization")
            allowCredentials = true
            maxAge = 7200L
        }

        return createCorsWebFilter(corsConfig)
    }

    private fun createCorsWebFilter(corsConfig: CorsConfiguration): CorsWebFilter {
        val source = UrlBasedCorsConfigurationSource().apply {
            registerCorsConfiguration("/**", corsConfig)
        }
        return CorsWebFilter(source)
    }
}

3. 监控和调试

kotlin
@Component
class CorsDebugFilter : GlobalFilter, Ordered {

    private val logger = LoggerFactory.getLogger(CorsDebugFilter::class.java)

    override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
        val request = exchange.request

        // 记录跨域请求信息
        if (request.headers.origin != null) {
            logger.debug(
                "CORS Request - Origin: {}, Method: {}, Path: {}",
                request.headers.origin,
                request.method,
                request.path.value()
            )
        }

        return chain.filter(exchange).doOnSuccess {
            // 记录响应中的 CORS 头
            val response = exchange.response
            logger.debug(
                "CORS Response Headers - Access-Control-Allow-Origin: {}, Methods: {}",
                response.headers.accessControlAllowOrigin,
                response.headers.accessControlAllowMethods
            )
        }
    }

    override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE
}

故障排查

常见问题及解决方案

  1. 预检请求失败
kotlin
// 确保配置了 OPTIONS 方法
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")

// 启用简单 URL 处理器映射
spring:
  cloud:
    gateway:
      globalcors:
        add-to-simple-url-handler-mapping: true
  1. 凭证传递问题
kotlin
// 检查 allowCredentials 配置
allowCredentials = true

// 确保前端正确设置
// JavaScript: fetch(url, { credentials: 'include' })
// Axios: axios.defaults.withCredentials = true
  1. 多重 CORS 配置冲突

如果同时配置了全局 CORS 和路由级 CORS,路由级配置会覆盖全局配置。确保配置的一致性。

总结

Spring Cloud Gateway 的 CORS 配置提供了灵活且强大的跨域解决方案。通过合理的配置策略,我们可以:

  • 🛡️ 安全性:在网关层统一处理跨域问题,避免安全漏洞
  • 🔧 灵活性:支持全局和路由级两种配置方式
  • 🎯 精确性:针对不同服务设置不同的 CORS 策略
  • 📊 可维护性:集中管理所有跨域配置,便于维护

在实际项目中,建议根据业务需求选择合适的配置方式,并始终将安全性放在首位,特别是在生产环境中要避免过于宽松的 CORS 配置。