代理后域名及Https协议向后传递,后端Spring获取不到问题记录及分析
项目使用前后端分离开发,前后端都部署在k8s中。
项目场景:
项目使用前后端分离开发,前后端都部署在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
包裹了请求对象,并且在初始化时使用UriComponentsBuilder
的adaptFromForwardedHeaders
方法处理了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);
这行上,在UseForwardHeaders
为true
的时候就会给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容器实现的差异,更加通用一些吧。
使用容器处理的时候,就需要特别注意下tomcat
、undertow
、jetty
对于请求是一个什么样的处理方式,以及SpringBoot的配置是不是可以覆盖的你所使用的容器。
更多推荐
所有评论(0)