Appearance
跨域配置
概述
在现代微服务架构中,跨域资源共享(CORS)是一个不可避免的问题。当前端应用部署在不同的域名或端口下,需要访问后端 API 时,浏览器会执行同源策略检查,阻止跨域请求。Spring Cloud Gateway 作为微服务架构中的网关组件,提供了灵活的 CORS 配置方案来解决这个问题。
CORS(Cross-Origin Resource Sharing)是一种机制,它允许 Web 页面从不同的域访问资源。Gateway 的 CORS 配置可以在网关层面统一处理跨域问题,避免在每个微服务中重复配置。
CORS 配置方式
Spring Cloud Gateway 提供了两种 CORS 配置方式:
- 全局 CORS 配置 - 对所有路由生效
- 路由级 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: 3600TIP
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 | 是否允许携带凭证 | true | 与 allowedOrigins: "*" 冲突 |
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
}故障排查
常见问题及解决方案
- 预检请求失败
kotlin
// 确保配置了 OPTIONS 方法
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 启用简单 URL 处理器映射
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true- 凭证传递问题
kotlin
// 检查 allowCredentials 配置
allowCredentials = true
// 确保前端正确设置
// JavaScript: fetch(url, { credentials: 'include' })
// Axios: axios.defaults.withCredentials = true- 多重 CORS 配置冲突
如果同时配置了全局 CORS 和路由级 CORS,路由级配置会覆盖全局配置。确保配置的一致性。
总结
Spring Cloud Gateway 的 CORS 配置提供了灵活且强大的跨域解决方案。通过合理的配置策略,我们可以:
- 🛡️ 安全性:在网关层统一处理跨域问题,避免安全漏洞
- 🔧 灵活性:支持全局和路由级两种配置方式
- 🎯 精确性:针对不同服务设置不同的 CORS 策略
- 📊 可维护性:集中管理所有跨域配置,便于维护
在实际项目中,建议根据业务需求选择合适的配置方式,并始终将安全性放在首位,特别是在生产环境中要避免过于宽松的 CORS 配置。