Appearance
PreserveHostHeader 网关过滤器工厂
概述
PreserveHostHeader 是 Spring Cloud Gateway 提供的一个重要的网关过滤器工厂,它的主要作用是保留原始的 Host 头部信息,而不是使用 HTTP 客户端重新确定的 Host 头部。这个过滤器在微服务架构中处理域名转发、负载均衡和服务发现场景时非常重要。
IMPORTANT
PreserveHostHeader 过滤器没有任何参数,它通过设置请求属性来通知路由过滤器保留原始的 Host 头部。
解决的问题
在微服务架构中,当客户端请求通过网关转发到后端服务时,默认情况下 HTTP 客户端会根据目标 URI 重新设置 Host 头部。这可能导致以下问题:
问题场景
- 域名丢失问题:客户端访问
api.example.com,但后端服务收到的 Host 可能是内部服务名 - 虚拟主机识别失败:后端服务依赖 Host 头部进行虚拟主机路由
- CORS 策略失效:跨域配置基于原始域名,Host 变更会导致 CORS 检查失败
- 日志追踪困难:原始请求域名信息丢失,影响问题排查
实际业务场景
场景一:多租户 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.comproperties
# 基础配置
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` 没有配置参数,但它的影响是全局性的。在使用时要充分考虑对整个请求处理流程的影响,特别是与其他过滤器和负载均衡器的交互。
通过合理使用这个过滤器,我们可以构建更加灵活和透明的微服务网关架构。