项目场景:

项目使用前后端分离开发,前后端都部署在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 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); 这行上,在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.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容器实现的差异,更加通用一些吧。

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

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

  1. nginx代理的域名对应的ip更换后还解析到之前的ip

    问题 使用nginx做反向代理,将请求发送到一个域名(例如: proxy_pass http://www.test.com 该域名对应的IP是A) ,刚开始运行一切正常,但是当运行了一段时间以后,域名 ...

  2. 二级域名,https协议的申请配置

    1. 申请二级域名: 登录阿里账号,点击域名,解析域名,添加解析,加入前缀,ip 即可 2. 申请ca认证书(实现安全协议https访问): 登录阿里云账号  ,点击ca认证申请 ,购买,选择免费,提 ...

  3. 访问网站,http、https协议抓包,完整分析

    HTTP.HTTPS协议 一.www.qq.com抓包 第一步:浏览器分析超链接中的URL www.qq.com 第二步:DNS请求 PC用本地IP地址向DNS服务器222.172.200.68发出D ...

  4. 穿刺检查、代理http proxy、https proxy、Socks,代理本质上就是一个中介

    作用 HTTP代理服务器 代理服务器(Proxy Server)功能就是代理网络用户去获得网络信息,形象点说就是网络信息的中转站. 用户客户机(client) <--> 代理服务器(pro ...

  5. 【计算机网络】--- HTTP与HTTPS协议详解

    HTTP与HTTPS协议详解 一.URL 二.HTTP协议 三.HTTPS协议 四.HTTP与HTTPS区别(重中之重) 五.如何正确选择HTTP协议和HTTPS协议 引言:当我们打开一个网页时,奇妙 ...

  6. 阿里云域名启用HTTPS

    想的再多,不去实践,就永远只是想想而已.   现在,HTTPS很火,而且谷歌大力推行,百度也积极收录.所以,网站从HTTP转向HTTPS是必然的.作为个人用户,一般有两种选择 域名服务商那里申请证书 ...

  7. 网络(9)-HTTPS协议

    一.HTTPS的概念 HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础 ...

  8. PB使用http协议、https协议(简单便捷)

    PB使用HTTP协议.HTTPS协议 PB自身也有http组件,但使用起来较为繁琐.VDN作者将http功能通过API的形式封装为HttpClient组件,PB直接调用即可,支持http及https协 ...

  9. 聊聊代理ip常见的三大协议。

    代理IP协议是针对分组交换计算机通信网中互连系统而设计的.代理人IP层只负责数据的路由和传输,不负责数据内容的处理,将数据报告发送到源节点和目标节点之间.为了使数据报表中必须有明确的目的地,每一个数据 ...

最新文章

  1. 2018ACM四川省赛G.Grisaia(超棒的杜教筛好题)
  2. 站立潮头、无问西东 | 第二届“大数据在清华”高峰论坛成功举办
  3. C++反汇编第五讲,认识C++中的Try catch语法,以及在反汇编中还原
  4. 憋不住的心里的一个想法,JVM的BYTECODE是完全平台无关的么?
  5. openwrt dhcp不分配_【装维技巧】DHCP工作原理详解(上)
  6. 自由自在休闲食品意式手工冰淇淋 百变不离健康
  7. 职场警示录:栽在邮件上的N种死法
  8. LintCode 1917. 切割剩余金属
  9. mysql 授权类型_MySQL-02-授权及数据类型
  10. 手机怎么识别图片中的文字?来试试这两个方法吧
  11. Android 手机如何改造成 Linux 服务器?
  12. 如何计算置信区间,RMSE均方根误差/标准误差:误差平方和的平均数开方
  13. 求购二手《良葛格Java JDK 5.0学习笔记》
  14. 电信物联网平台ctwing对接开发-平台概述
  15. 《谁说菜鸟不会数据分析 入门篇》学习笔记
  16. C++实现暴力筛、朴素素数筛、埃氏素数筛、欧拉素数筛的解法
  17. 【消息中心】架构准备
  18. 牛客网-精华专题-前端校招面试题目合集
  19. Ubuntu系统下安装SQLite Browser教程
  20. 偏前端 + rsa加解密 + jsencrypt.min.js--(新增超长字符分段加解密)

热门文章

  1. 电路的基本概念(1) 自学笔记
  2. PYQT5|一键自动生成并应用QRC资源文件
  3. 基于51单片机的倒计时温度检测报警器
  4. 免费使用OriginPro学习版
  5. Linux远程SSH终端和文件传输工具
  6. 「订单」业务的设计与实现
  7. springMVC + Dubbo + zooKeeper超详细 步骤
  8. GetCheckedRadioButton
  9. web前端 | 一条“不归路” - 学习路线
  10. 《数据结构》实验报告二:顺序表 链表