代理后域名及Https协议向后传递,后端Spring获取不到问题记录及分析

2 篇文章 0 订阅
2 篇文章 0 订阅

项目场景:

项目使用前后端分离开发,前后端都部署在k8s中。

前端

前端项目通过nginx代理到后端服务器。
nginx中配置了如下Header:

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto  $scheme;

后端

后端项目使用了SpringBoot并使用了Undertow作为Servlet容器


问题描述

现在有两个服务 A 服务使用了springboot 2.3.4,B 服务使用了springboot 2.0.8。这两个服务的配置相同。

A服务可以通过HttpServletRequest 获取 https 及实际请求的域名
B服务通过HttpServletRequest 获取不到请求的协议和实际请求的域名


原因分析:

通过查看springboot项目源码发现,在处理这些Forward的时候Spring提供了两种方式去处理。

1.使用Spring提供的Filter ForwardedHeaderFilter

在这个Filter中使用ForwardedHeaderExtractingRequest包裹了请求对象,并且在初始化时使用UriComponentsBuilderadaptFromForwardedHeaders 方法处理了Forward请求头信息

处理方法如下:

UriComponentsBuilder adaptFromForwardedHeaders(HttpHeaders headers) {
		try {
			String forwardedHeader = headers.getFirst("Forwarded");
			if (StringUtils.hasText(forwardedHeader)) {
				String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0];
				Matcher matcher = FORWARDED_PROTO_PATTERN.matcher(forwardedToUse);
				if (matcher.find()) {
					scheme(matcher.group(1).trim());
					port(null);
				}
				matcher = FORWARDED_HOST_PATTERN.matcher(forwardedToUse);
				if (matcher.find()) {
					adaptForwardedHost(matcher.group(1).trim());
				}
			}
			else {
				String protocolHeader = headers.getFirst("X-Forwarded-Proto");
				if (StringUtils.hasText(protocolHeader)) {
					scheme(StringUtils.tokenizeToStringArray(protocolHeader, ",")[0]);
					port(null);
				}

				String hostHeader = headers.getFirst("X-Forwarded-Host");
				if (StringUtils.hasText(hostHeader)) {
					adaptForwardedHost(StringUtils.tokenizeToStringArray(hostHeader, ",")[0]);
				}

				String portHeader = headers.getFirst("X-Forwarded-Port");
				if (StringUtils.hasText(portHeader)) {
					port(Integer.parseInt(StringUtils.tokenizeToStringArray(portHeader, ",")[0]));
				}
			}
		}
		catch (NumberFormatException ex) {
			throw new IllegalArgumentException("Failed to parse a port from \"forwarded\"-type headers. " +
					"If not behind a trusted proxy, consider using ForwardedHeaderFilter " +
					"with the removeOnly=true. Request headers: " + headers);
		}

		if (this.scheme != null && ((this.scheme.equals("http") && "80".equals(this.port)) ||
				(this.scheme.equals("https") && "443".equals(this.port)))) {
			port(null);
		}

		return this;
	}

2.使用容器提供的处理能力(Undertow)

undertow在处理请求时提供了一系列的HttpHandler,其中有一个ProxyPeerAddressHandler用于处理Forward系列代理请求头。
代码如下:

public void handleRequest(HttpServerExchange exchange) throws Exception {
        String forwardedFor = exchange.getRequestHeaders().getFirst(Headers.X_FORWARDED_FOR);
        if (forwardedFor != null) {
            String remoteClient = mostRecent(forwardedFor);
            //we have no way of knowing the port
            if(IP4_EXACT.matcher(forwardedFor).matches()) {
                exchange.setSourceAddress(new InetSocketAddress(NetworkUtils.parseIpv4Address(remoteClient), 0));
            } else if(IP6_EXACT.matcher(forwardedFor).matches()) {
                exchange.setSourceAddress(new InetSocketAddress(NetworkUtils.parseIpv6Address(remoteClient), 0));
            } else {
                exchange.setSourceAddress(InetSocketAddress.createUnresolved(remoteClient, 0));
            }
        }
        String forwardedProto = exchange.getRequestHeaders().getFirst(Headers.X_FORWARDED_PROTO);
        if (forwardedProto != null) {
            exchange.setRequestScheme(mostRecent(forwardedProto));
        }
        String forwardedHost = exchange.getRequestHeaders().getFirst(Headers.X_FORWARDED_HOST);
        String forwardedPort = exchange.getRequestHeaders().getFirst(Headers.X_FORWARDED_PORT);
        if (forwardedHost != null) {
            String value = mostRecent(forwardedHost);
            if(value.startsWith("[")) {
                int end = value.lastIndexOf("]");
                if(end == -1 ) {
                    end = 0;
                }
                int index = value.indexOf(":", end);
                if(index != -1) {
                    forwardedPort = value.substring(index + 1);
                    value = value.substring(0, index);
                }
            } else {
                int index = value.lastIndexOf(":");
                if(index != -1) {
                    forwardedPort = value.substring(index + 1);
                    value = value.substring(0, index);
                }
            }
            int port = 0;
            String hostHeader = NetworkUtils.formatPossibleIpv6Address(value);
            if(forwardedPort != null) {
                try {
                    port = Integer.parseInt(mostRecent(forwardedPort));
                    if(port > 0) {
                        String scheme = exchange.getRequestScheme();

                        if (!standardPort(port, scheme)) {
                            hostHeader += ":" + port;
                        }
                    } else {
                        UndertowLogger.REQUEST_LOGGER.debugf("Ignoring negative port: %s", forwardedPort);
                    }
                } catch (NumberFormatException ignore) {
                    UndertowLogger.REQUEST_LOGGER.debugf("Cannot parse port: %s", forwardedPort);
                }
            }
            exchange.getRequestHeaders().put(Headers.HOST, hostHeader);
            exchange.setDestinationAddress(InetSocketAddress.createUnresolved(value, port));
        }
        next.handleRequest(exchange);
    }

为什么SpringBoot2.0.8获取不到实际域名和协议而SpringBoot2.3.4没问题呢?

1.ForwardedHeaderFilter 为什么没生效?

1.1 SpringBoot 2.0.8

在2.0.8中并没有找到自动配置ForwardedHeaderFilter 的地方,如果要使用这个Filter需要自己添加Filter配置

1.2 SpringBoot 2.3.4

在SpringBoot2.3.4的ServletWebServerFactoryAutoConfiguration这个自动配置类中,可以看到新增加了如下内容:

	@Bean
	// 在丢失这个Filter注册的实例时创建这个实例
    @ConditionalOnMissingFilterBean({ForwardedHeaderFilter.class})
    // 在配置这个属性的使用,且为 `framework` 时,创建这个实例
    // 在这里多个Conditional注解是 并且的关系
    @ConditionalOnProperty(
        value = {"server.forward-headers-strategy"},
        havingValue = "framework"
    )
    public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
        ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
        FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean(filter, new ServletRegistrationBean[0]);
        registration.setDispatcherTypes(DispatcherType.REQUEST, new DispatcherType[]{DispatcherType.ASYNC, DispatcherType.ERROR});
        registration.setOrder(Integer.MIN_VALUE);
        return registration;
    }

1.3 小结

使用这个Filter需要增加配置或者 声明Bean实例,在项目中并没有这些配置,所以这个Filter不生效。

2. 为什么Undertow的 ProxyPeerAddressHandler SpringBoot 2.3.4生效,SpringBoot 2.0.8不生效嘞?

原因就处在UndertowWebServerFactoryCustomizer 这个配置类中

    public void customize(ConfigurableUndertowWebServerFactory factory) {
        PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
        ServerOptions options = new ServerOptions(factory);
        ServerProperties properties = this.serverProperties;
        properties.getClass();
        map.from(properties::getMaxHttpHeaderSize).asInt(DataSize::toBytes).when(this::isPositive).to(options.option(UndertowOptions.MAX_HEADER_SIZE));
        this.mapUndertowProperties(factory, options);
        this.mapAccessLogProperties(factory);
        map.from(this::getOrDeduceUseForwardHeaders).to(factory::setUseForwardHeaders);
    }

这段代码问题就出在map.from(this::getOrDeduceUseForwardHeaders).to(factory::setUseForwardHeaders); 这行上,在UseForwardHeaderstrue的时候就会给Undertow注册ProxyPeerAddressHandler 这个处理器,再看下getOrDeduceUseForwardHeaders 源码

// 2.0.8
private boolean getOrDeduceUseForwardHeaders() {
        if (this.serverProperties.isUseForwardHeaders() != null) {
            return this.serverProperties.isUseForwardHeaders();
        } else {
            CloudPlatform platform = CloudPlatform.getActive(this.environment);
            return platform != null && platform.isUsingForwardHeaders();
        }
    }
 // 2.3.4
 private boolean getOrDeduceUseForwardHeaders() {
        if (this.serverProperties.getForwardHeadersStrategy() != null) {
            return this.serverProperties.getForwardHeadersStrategy().equals(ForwardHeadersStrategy.NATIVE);
        } else {
            CloudPlatform platform = CloudPlatform.getActive(this.environment);
            return platform != null && platform.isUsingForwardHeaders();
        }
    }

可以看到这里如果没有配置ForwardHeadersStrategy 策略的时候,SpringBoot判断了下当前运行环境是不是云平台,如果是的话,就使用platform的isUsingForwardHeaders 返回参数,而isUsingForwardHeaders 这个方法直接就写死了返回true

那问题就出在匹配云平台这一步上了。

再看下这个getActive方法,CloudPlatform 是一个枚举类。

public static CloudPlatform getActive(Environment environment) {
	if (environment != null) {
		for (CloudPlatform cloudPlatform : values()) {
			if (cloudPlatform.isActive(environment)) {
				return cloudPlatform;
			}
		}
	}
	return null;
}

这里循环了所以配置的云平台,然后使用isActive 方法 进行匹配。

对比下这两个SpringBoot版本的CloudPlatform

// 2.3.4
public enum CloudPlatform {

	/**
	 * No Cloud platform. Useful when false-positives are detected.
	 */
	NONE {
			...
	},

	/**
	 * Cloud Foundry platform.
	 */
	CLOUD_FOUNDRY {
			...
	},

	/**
	 * Heroku platform.
	 */
	HEROKU {
				...
	},
	/**
	 * SAP Cloud platform.
	 */
	SAP {
		...
	},

	/**
	 * Kubernetes platform.
	 */
	KUBERNETES {
				...
	};
		...
}
// 2.0.8
public enum CloudPlatform {
	/**
	 * Cloud Foundry platform.
	 */
	CLOUD_FOUNDRY {
		...
	},
	/**
	 * Heroku platform.
	 */
	HEROKU {
		...
	},
	/**
	 * SAP Cloud platform.
	 */
	SAP {
	...
	};
	...
}

可以看到 这两个版本对比,就是 2.3.4中多了个KUBERNETES 的枚举。

小结

查了上边一堆的代码后,就是SpringBoot 2.3.4 中多了个 云平台的枚举,而由于我们项目部署环境就是k8s,所以在什么都不配置的情况下 使用2.3.4 的项目直接就可以生效。2.0.8 的项目就需要增加配置了。


解决方案:

1.使用Spring提供的ForwardedHeaderFilter

1.1 2.0.8

找个@Configuration注解的类增加如下配置。

	@Bean
    public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
        ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
        FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean(filter, new ServletRegistrationBean[0]);
        registration.setDispatcherTypes(DispatcherType.REQUEST, new DispatcherType[]{DispatcherType.ASYNC, DispatcherType.ERROR});
        registration.setOrder(Integer.MIN_VALUE);
        return registration;
    }

1.2 2.3.4+

可以通过2.0.8的方式声明Bean实例。
或者增加配置,使自动配置生效。

server:
	forward-headers-strategy: FRAMEWORK

2.使用 容器方案

2.1 2.0.8

增加如下配置:

server:
	useForwardHeaders: true

2.2 2.3.4+

增加如下配置:

server:
	forward-headers-strategy: NATIVE

3.对比两种方式。

使用Spring提供的方案,可以忽略Servlet容器实现的差异,更加通用一些吧。

使用容器处理的时候,就需要特别注意下tomcatundertowjetty对于请求是一个什么样的处理方式,以及SpringBoot的配置是不是可以覆盖的你所使用的容器。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值