代理后域名及Https协议向后传递,后端Spring获取不到问题记录及分析
项目场景:
项目使用前后端分离开发,前后端都部署在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 portif(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.4private 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注解的类增加如下配置。
@Beanpublic 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的配置是不是可以覆盖的你所使用的容器。
代理后域名及Https协议向后传递,后端Spring获取不到问题记录及分析相关推荐
- nginx代理的域名对应的ip更换后还解析到之前的ip
问题 使用nginx做反向代理,将请求发送到一个域名(例如: proxy_pass http://www.test.com 该域名对应的IP是A) ,刚开始运行一切正常,但是当运行了一段时间以后,域名 ...
- 二级域名,https协议的申请配置
1. 申请二级域名: 登录阿里账号,点击域名,解析域名,添加解析,加入前缀,ip 即可 2. 申请ca认证书(实现安全协议https访问): 登录阿里云账号 ,点击ca认证申请 ,购买,选择免费,提 ...
- 访问网站,http、https协议抓包,完整分析
HTTP.HTTPS协议 一.www.qq.com抓包 第一步:浏览器分析超链接中的URL www.qq.com 第二步:DNS请求 PC用本地IP地址向DNS服务器222.172.200.68发出D ...
- 穿刺检查、代理http proxy、https proxy、Socks,代理本质上就是一个中介
作用 HTTP代理服务器 代理服务器(Proxy Server)功能就是代理网络用户去获得网络信息,形象点说就是网络信息的中转站. 用户客户机(client) <--> 代理服务器(pro ...
- 【计算机网络】--- HTTP与HTTPS协议详解
HTTP与HTTPS协议详解 一.URL 二.HTTP协议 三.HTTPS协议 四.HTTP与HTTPS区别(重中之重) 五.如何正确选择HTTP协议和HTTPS协议 引言:当我们打开一个网页时,奇妙 ...
- 阿里云域名启用HTTPS
想的再多,不去实践,就永远只是想想而已. 现在,HTTPS很火,而且谷歌大力推行,百度也积极收录.所以,网站从HTTP转向HTTPS是必然的.作为个人用户,一般有两种选择 域名服务商那里申请证书 ...
- 网络(9)-HTTPS协议
一.HTTPS的概念 HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础 ...
- PB使用http协议、https协议(简单便捷)
PB使用HTTP协议.HTTPS协议 PB自身也有http组件,但使用起来较为繁琐.VDN作者将http功能通过API的形式封装为HttpClient组件,PB直接调用即可,支持http及https协 ...
- 聊聊代理ip常见的三大协议。
代理IP协议是针对分组交换计算机通信网中互连系统而设计的.代理人IP层只负责数据的路由和传输,不负责数据内容的处理,将数据报告发送到源节点和目标节点之间.为了使数据报表中必须有明确的目的地,每一个数据 ...
最新文章
- 2018ACM四川省赛G.Grisaia(超棒的杜教筛好题)
- 站立潮头、无问西东 | 第二届“大数据在清华”高峰论坛成功举办
- C++反汇编第五讲,认识C++中的Try catch语法,以及在反汇编中还原
- 憋不住的心里的一个想法,JVM的BYTECODE是完全平台无关的么?
- openwrt dhcp不分配_【装维技巧】DHCP工作原理详解(上)
- 自由自在休闲食品意式手工冰淇淋 百变不离健康
- 职场警示录:栽在邮件上的N种死法
- LintCode 1917. 切割剩余金属
- mysql 授权类型_MySQL-02-授权及数据类型
- 手机怎么识别图片中的文字?来试试这两个方法吧
- Android 手机如何改造成 Linux 服务器?
- 如何计算置信区间,RMSE均方根误差/标准误差:误差平方和的平均数开方
- 求购二手《良葛格Java JDK 5.0学习笔记》
- 电信物联网平台ctwing对接开发-平台概述
- 《谁说菜鸟不会数据分析 入门篇》学习笔记
- C++实现暴力筛、朴素素数筛、埃氏素数筛、欧拉素数筛的解法
- 【消息中心】架构准备
- 牛客网-精华专题-前端校招面试题目合集
- Ubuntu系统下安装SQLite Browser教程
- 偏前端 + rsa加解密 + jsencrypt.min.js--(新增超长字符分段加解密)