Skip to content

PreserveHostHeader 网关过滤器工厂

概述

PreserveHostHeader 是 Spring Cloud Gateway 提供的一个重要的网关过滤器工厂,它的主要作用是保留原始的 Host 头部信息,而不是使用 HTTP 客户端重新确定的 Host 头部。这个过滤器在微服务架构中处理域名转发、负载均衡和服务发现场景时非常重要。

IMPORTANT

PreserveHostHeader 过滤器没有任何参数,它通过设置请求属性来通知路由过滤器保留原始的 Host 头部。

解决的问题

在微服务架构中,当客户端请求通过网关转发到后端服务时,默认情况下 HTTP 客户端会根据目标 URI 重新设置 Host 头部。这可能导致以下问题:

问题场景

  1. 域名丢失问题:客户端访问 api.example.com,但后端服务收到的 Host 可能是内部服务名
  2. 虚拟主机识别失败:后端服务依赖 Host 头部进行虚拟主机路由
  3. CORS 策略失效:跨域配置基于原始域名,Host 变更会导致 CORS 检查失败
  4. 日志追踪困难:原始请求域名信息丢失,影响问题排查

实际业务场景

场景一:多租户 SaaS 平台

假设你运营一个多租户的 SaaS 平台,不同的客户使用不同的子域名访问:

  • tenant1.api.example.com → 租户 1 的服务
  • tenant2.api.example.com → 租户 2 的服务
  • tenant3.api.example.com → 租户 3 的服务

场景二:反向代理和负载均衡

在企业内网环境中,网关作为反向代理,需要保持客户端的原始访问域名:

配置方式

YAML 配置

yaml
spring:
  cloud:
    gateway:
      routes:
        # 基础配置示例
        - id: preserve_host_route
          uri: https://example.org
          filters:
            - PreserveHostHeader
          predicates:
            - Path=/api/**

        # 多租户场景配置
        - id: tenant_route
          uri: lb://user-service # 使用服务发现
          filters:
            - PreserveHostHeader
            - StripPrefix=1
          predicates:
            - Host=*.api.example.com
            - Path=/api/**

        # 企业内网反向代理
        - id: internal_proxy
          uri: http://internal-service:8080
          filters:
            - PreserveHostHeader
            - AddRequestHeader=X-Gateway-Source, spring-cloud-gateway
          predicates:
            - Host=internal.company.com
properties
# 基础配置
spring.cloud.gateway.routes[0].id=preserve_host_route
spring.cloud.gateway.routes[0].uri=https://example.org
spring.cloud.gateway.routes[0].filters[0]=PreserveHostHeader
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**

# 多租户配置
spring.cloud.gateway.routes[1].id=tenant_route
spring.cloud.gateway.routes[1].uri=lb://user-service
spring.cloud.gateway.routes[1].filters[0]=PreserveHostHeader
spring.cloud.gateway.routes[1].filters[1]=StripPrefix=1
spring.cloud.gateway.routes[1].predicates[0]=Host=*.api.example.com
spring.cloud.gateway.routes[1].predicates[1]=Path=/api/**

Java/Kotlin 编程式配置

kotlin
@Configuration
@EnableConfigurationProperties
class GatewayRouteConfiguration {

    /**
     * 配置保留 Host 头部的路由
     * 适用于多租户 SaaS 平台场景
     */
    @Bean
    fun preserveHostRoutes(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            // 多租户路由配置
            .route("tenant_preserve_host") { route ->
                route
                    .host("*.api.example.com")  // 匹配所有子域名
                    .and()
                    .path("/api/**")            // 匹配 API 路径
                    .filters { filter ->
                        filter
                            .preserveHostHeader()        // 保留原始 Host 头部
                            .stripPrefix(1)              // 移除路径前缀
                            .addRequestHeader(           // 添加自定义头部
                                "X-Gateway-Processed",
                                "true"
                            )
                    }
                    .uri("lb://user-service")    // 负载均衡到用户服务
            }
            // 反向代理路由配置
            .route("internal_proxy") { route ->
                route
                    .host("internal.company.com")
                    .and()
                    .path("/**")
                    .filters { filter ->
                        filter
                            .preserveHostHeader()        // 保留内网域名
                            .addRequestHeader(
                                "X-Original-Host",
                                "#{request.headers.host[0]}"
                            )
                    }
                    .uri("http://internal-backend:8080")
            }
            // 开发环境调试路由
            .route("debug_preserve_host") { route ->
                route
                    .host("localhost")
                    .and()
                    .path("/debug/**")
                    .filters { filter ->
                        filter
                            .preserveHostHeader()
                            .addRequestHeader("X-Debug-Mode", "true")
                            .addResponseHeader(
                                "X-Debug-Info",
                                "Host preserved: #{request.headers.host[0]}"
                            )
                    }
                    .uri("http://debug-service:9090")
            }
            .build()
    }

    /**
     * 自定义全局过滤器,配合 PreserveHostHeader 使用
     * 用于记录和监控 Host 头部的处理情况
     */
    @Bean
    fun hostHeaderLoggingFilter(): GlobalFilter {
        return GlobalFilter { exchange, chain ->
            val request = exchange.request
            val originalHost = request.headers.getFirst("Host")

            // 记录原始 Host 信息
            println("原始请求 Host: $originalHost")
            println("请求路径: ${request.path.value()}")
            println("PreserveHostHeader 属性: ${exchange.getAttribute<Boolean>("preserveHostHeader")}")

            chain.filter(exchange).doOnSuccess {
                // 请求完成后的日志记录
                println("请求处理完成,Host 头部已${if (exchange.getAttribute<Boolean>("preserveHostHeader") == true) "保留" else "重写"}")
            }
        }
    }
}
java
@Configuration
@EnableConfigurationProperties
public class GatewayRouteConfiguration {

    /**
     * 配置保留 Host 头部的路由
     */
    @Bean
    public RouteLocator preserveHostRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("tenant_preserve_host", route ->
                route.host("*.api.example.com")
                     .and()
                     .path("/api/**")
                     .filters(filter ->
                         filter.preserveHostHeader()
                               .stripPrefix(1)
                               .addRequestHeader("X-Gateway-Processed", "true")
                     )
                     .uri("lb://user-service")
            )
            .route("internal_proxy", route ->
                route.host("internal.company.com")
                     .and()
                     .path("/**")
                     .filters(filter ->
                         filter.preserveHostHeader()
                               .addRequestHeader("X-Original-Host", "#{request.headers.host[0]}")
                     )
                     .uri("http://internal-backend:8080")
            )
            .build();
    }
}

工作原理

PreserveHostHeader 过滤器的工作流程如下:

关键实现细节

请求属性设置

kotlin
/**
 * PreserveHostHeader 过滤器的核心实现原理
 */
class PreserveHostHeaderGatewayFilterFactory : AbstractGatewayFilterFactory<Any>() {

    override fun apply(config: Any): GatewayFilter {
        return GatewayFilter { exchange, chain ->
            // 设置请求属性,标记需要保留 Host 头部
            val newExchange = exchange.mutate()
                .request { request ->
                    request.build()
                }
                .build()

            // 设置保留 Host 头部的标记属性
            newExchange.attributes[PRESERVE_HOST_HEADER_ATTRIBUTE] = true

            // 继续过滤器链处理
            chain.filter(newExchange)
        }
    }

    companion object {
        // 用于标记的请求属性键
        const val PRESERVE_HOST_HEADER_ATTRIBUTE = "preserveHostHeader"
    }
}

路由过滤器检查

kotlin
/**
 * 路由过滤器中的 Host 头部处理逻辑
 */
class RouteToRequestUrlFilter : GlobalFilter {

    override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
        val preserveHostHeader = exchange.getAttribute<Boolean>("preserveHostHeader") ?: false

        return if (preserveHostHeader) {
            // 保留原始 Host 头部,不进行重写
            handleWithPreservedHost(exchange, chain)
        } else {
            // 使用默认的 Host 头部重写逻辑
            handleWithDefaultHost(exchange, chain)
        }
    }

    private fun handleWithPreservedHost(
        exchange: ServerWebExchange,
        chain: GatewayFilterChain
    ): Mono<Void> {
        val request = exchange.request
        val originalHost = request.headers.getFirst("Host")

        // 记录保留 Host 头部的日志
        logger.debug("保留原始 Host 头部: $originalHost")

        return chain.filter(exchange)
    }
}

最佳实践

1. 与其他过滤器的组合使用

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: comprehensive_route
          uri: lb://backend-service
          filters:
            # 按顺序执行的过滤器
            - PreserveHostHeader # 1. 保留 Host 头部
            - AddRequestHeader=X-Gateway-ID, gateway-01 # 2. 添加网关标识
            - AddRequestHeader=X-Original-Host, "#{request.headers.host[0]}" # 3. 记录原始 Host
            - StripPrefix=1 # 4. 移除路径前缀
            - CircuitBreaker=myCircuitBreaker # 5. 熔断保护
          predicates:
            - Host=**.api.example.com
            - Path=/api/**

2. 条件性启用

kotlin
@Configuration
class ConditionalPreserveHostConfiguration {

    /**
     * 根据环境变量条件性启用 PreserveHostHeader
     */
    @Bean
    fun conditionalPreserveHostRoute(
        builder: RouteLocatorBuilder,
        @Value("\${gateway.preserve-host:false}") preserveHost: Boolean
    ): RouteLocator {
        return builder.routes()
            .route("conditional_preserve_host") { route ->
                val filters = mutableListOf<String>()

                // 条件性添加 PreserveHostHeader 过滤器
                if (preserveHost) {
                    filters.add("PreserveHostHeader")
                }

                // 其他必要的过滤器
                filters.add("StripPrefix=1")
                filters.add("AddRequestHeader=X-Gateway-Version, 1.0")

                route
                    .path("/api/**")
                    .filters { filter ->
                        filters.forEach { filterName ->
                            when {
                                filterName == "PreserveHostHeader" ->
                                    filter.preserveHostHeader()
                                filterName.startsWith("StripPrefix") ->
                                    filter.stripPrefix(1)
                                filterName.startsWith("AddRequestHeader") -> {
                                    val parts = filterName.split("=", limit = 3)
                                    filter.addRequestHeader(parts[1], parts[2])
                                }
                            }
                        }
                        filter
                    }
                    .uri("lb://backend-service")
            }
            .build()
    }
}

3. 监控和调试

kotlin
@Component
class HostHeaderMonitoringFilter : GlobalFilter, Ordered {

    private val logger = LoggerFactory.getLogger(HostHeaderMonitoringFilter::class.java)
    private val meterRegistry = Metrics.globalRegistry

    override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
        val request = exchange.request
        val originalHost = request.headers.getFirst("Host")
        val preserveHostHeader = exchange.getAttribute<Boolean>("preserveHostHeader") ?: false

        // 记录指标
        meterRegistry.counter(
            "gateway.host.header.preserved",
            "preserved", preserveHostHeader.toString(),
            "original_host", originalHost ?: "unknown"
        ).increment()

        // 详细日志记录
        logger.info("""
            Host 头部处理信息:
            - 原始 Host: $originalHost
            - 保留设置: $preserveHostHeader
            - 请求路径: ${request.path.value()}
            - 请求方法: ${request.method}
        """.trimIndent())

        return chain.filter(exchange)
    }

    override fun getOrder(): Int = -1  // 高优先级执行
}

常见问题和解决方案

问题 1:Host 头部仍然被重写

如果发现 Host 头部仍然被重写,请检查过滤器的执行顺序。

解决方案:

kotlin
/**
 * 确保 PreserveHostHeader 过滤器在路由过滤器之前执行
 */
@Bean
fun customGatewayFilterChain(): GlobalFilter {
    return GlobalFilter { exchange, chain ->
        // 强制设置保留 Host 头部属性
        exchange.attributes["preserveHostHeader"] = true

        chain.filter(exchange)
    }
}

问题 2:与负载均衡冲突

使用服务发现(如 `lb://service-name`)时,确保服务注册信息正确。

解决方案:

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: lb_preserve_host
          uri: lb://user-service
          filters:
            - PreserveHostHeader
            # 添加自定义负载均衡策略
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenish-rate: 10
                redis-rate-limiter.burst-capacity: 20

问题 3:HTTPS 重定向问题

在 HTTPS 环境中,注意 Host 头部可能影响 SSL 证书验证。

解决方案:

kotlin
@Configuration
class HttpsHostConfiguration {

    @Bean
    fun httpsPreserveHostRoute(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("https_preserve_host") { route ->
                route
                    .host("secure.api.example.com")
                    .and()
                    .header("X-Forwarded-Proto", "https")  // 确保 HTTPS 协议
                    .filters { filter ->
                        filter
                            .preserveHostHeader()
                            .addRequestHeader("X-Forwarded-Host", "#{request.headers.host[0]}")
                            .addRequestHeader("X-Forwarded-Proto", "https")
                    }
                    .uri("https://secure-backend:8443")
            }
            .build()
    }
}

总结

PreserveHostHeader 过滤器是 Spring Cloud Gateway 中一个简单但非常重要的工具,它解决了微服务架构中 Host 头部信息丢失的问题。通过保留原始的 Host 头部,我们可以:

  • ✅ 支持多租户架构的域名识别
  • ✅ 保持反向代理的透明性
  • ✅ 确保后端服务的虚拟主机路由正常工作
  • ✅ 维护完整的请求链路追踪信息

虽然 `PreserveHostHeader` 没有配置参数,但它的影响是全局性的。在使用时要充分考虑对整个请求处理流程的影响,特别是与其他过滤器和负载均衡器的交互。

通过合理使用这个过滤器,我们可以构建更加灵活和透明的微服务网关架构。