目录
解决方法三(tomcat新版本,nginx只能配置http):
第二点:需要给请求头中的X-Forwarded-Proto设置合适的值
问题背景
在使用saml2.0整合单点登录的过程中,如果项目是在同一个容器中(Tomcat)是没有问题的。但是,如果涉及到前后端分离,从而使用Nginx代理转发的情况,会分别发生由于端口不一致,http请求方式不一样造成的错误。下面谈谈如何解决,以及背后涉及到的原理。
这里说下Tomcat版本问题,笔者使用9.0版本,为下文所述新版本,旧版本未测试,只是通过tomcat配置代码反推出来的,至于具体版本不清楚。
问题解决
端口不匹配
解决方法一(tomcat老版本):
此种方法主要针对老版本的tomcat,主要解决转发端口和程序运行端口不匹配问题,例如nginx以80转发,程序以81端口运行:
server:
tomcat:
internal-proxies: 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.1[6-9]{1}\.\d{1,3}\.\d{1,3}|172\.2[0-9]{1}\.\d{1,3}\.\d{1,3}|172\.3[0-1]{1}\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1|::1
protocol-header: X-Forwarded-Proto
还没完,需要配合nginx配置进行一些请求头设置:
location / {
proxy_set_header X-Forwarded-Proto http;
proxy_pass http://localhost:81/;
index index.html index.htm;
}
其原理是通过X-Forwarded-Proto指定以http的形式发送请求。注意在老版本的情况下,不支持通过X-Forwarded-Port改写默认的http请求的80端口,只能走默认端口80。
解决方法二(tomcat新版本):
上面的那种方式,springboot官方已经不推荐使用,目前推荐以remoteIp的形式进行配置,但两者的后台配置原理是一致的:
server:
tomcat:
remoteip:
protocol-header: X-Forwarded-Proto
对应nginx配置基本保持一致,区别是可以通过X-Forwarded-Port配置任意端口,这里nginx的转发端口是8080,项目实际运行在8081:
location / {
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Forwarded-Port 8080;
proxy_pass http://localhost:8081/;
index index.html index.htm;
}
http请求方式不匹配
上面主要是介绍了http形式下的端口转发,一般多用于开发环境,在实际的生产过程中,通常我们会启用https请求,假如项目仍旧运行在一个tomcat上,且以tomcat的方式启用了https,理论上是不需要配置的。
但以目前的趋势来看,前后端分离,甚至是基于云服务的基础设施的完善,更多的情况是,ssl证书配置在了nginx或者例如AWS的alb等负载均衡中间件,这就会造成,在于外界交互认证的过程中,可以指定以https的请求到负载均衡中间件,但是当完成认证,进行服务跳转的时候,是直接发生在tomcat服务器内部的,走的却是默认的http请求方式。
这依旧会生成如标题所述错误。
解决方法一(tomcat老版本):
Java配置基本没有变化:
server:
tomcat:
internal-proxies: 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.1[6-9]{1}\.\d{1,3}\.\d{1,3}|172\.2[0-9]{1}\.\d{1,3}\.\d{1,3}|172\.3[0-1]{1}\.\d{1,3}\.\d{1,3}|0:0:0:0:0:0:0:1|::1
protocol-header: X-Forwarded-Proto
变化在于nginx的配置,且当前的https只支持443端口,也就是说nginx只能配置443端口:
location / {
#注意这里主要是X-Forwarded-Proto换成了https
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://localhost:81/;
index index.html index.htm;
}
解决方法二(tomcat新版本,生产推荐):
Java配置可以保持不变:
server:
tomcat:
remoteip:
protocol-header: X-Forwarded-Proto
对应nginx配置基本保持一致,区别是可以通过-Forwarded-Port配置任意端口,这里nginx的转发端口是8080,项目实际运行在8081,当然还有最重要的,X-Forwarded-Proto要是https:
location / {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 8080;
proxy_pass http://localhost:8081/;
index index.html index.htm;
}
解决方法三(tomcat新版本,nginx只能配置http):
有时候,nginx的配置我们改不了,或者说多部门协助不方便改,可以保持nginx配置不变,依旧为http:
location / {
proxy_set_header X-Forwarded-Proto http;
proxy_set_header X-Forwarded-Port 8080;
proxy_pass http://localhost:8081/;
index index.html index.htm;
}
Java配置稍微修改:
server:
tomcat:
remoteip:
protocol-header: X-Forwarded-Proto
#设置启用https的方式,默认是需要https
protocol-header-https-value: http
也可以实现对转发请求的修改。
原理解析
授人以鱼不如授人以渔,各种配置纷繁复杂,稍微错一点也不知道该怎么办,只能像无头苍蝇一样到处乱试,那只有掌握了底层原理,才能对一切问题游刃有余。
上面配置的核心原理是,通过yml文件中,server.tomcat下的配置,来开启tomcat的流程引擎——RemoteIpValve,在tomcat转发请求的过程中,把RemoteIpValve放在原本的默认引擎——StandardEngineValve的前面执行,来执行一系列的http请求替换操作。
这里tomcat的流程引擎是tomcat管道——Pipeline中的一组链式引擎,类似于Servlet中的Filter链式调用,都是以链表的形式,在特定的步骤中,串起一系列同一属性的执行任务。
默认的tomcat流程引擎只有一个,也称为Pipeline的basic引擎,在执行tomcat的过程中,会取first引擎来开始,按顺序执行,如果只有一个basic引擎,会以basic为first引擎来执行tomcat请求。
postParseSuccess = this.postParseRequest(req, request, res, response);
if (postParseSuccess) {
request.setAsyncSupported(this.connector.getService().getContainer().getPipeline().isAsyncSupported());
//取第一个引擎执行请求
this.connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
如果开启了tomcat配置,就会使用RemoteIpValve的invoke方法,来实现ip和端口替换。
RemoteIpValve重点
在执行RemoteIpValve的invoke方法中,有两点需要注意。
第一点:需要把当前请求中的ip地址是可信任ip
boolean isInternal = this.internalProxies != null && this.internalProxies.matcher(originalRemoteAddr).matches();
internalProxies就是在上面配置的:
但是如果是使用remoteIp配置,这个internal-proxies帮我们默认配置好了,基本可以满足需求,但是也支持自定义配置:
@ConfigurationProperties(
prefix = "server",
ignoreUnknownFields = true
)
public class ServerProperties {
private Integer port;
private InetAddress address;
@NestedConfigurationProperty
private final ErrorProperties error = new ErrorProperties();
private ServerProperties.ForwardHeadersStrategy forwardHeadersStrategy;
private String serverHeader;
......
public static class Remoteip {
private String internalProxies = "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|192\\.168\\.\\d{1,3}\\.\\d{1,3}|169\\.254\\.\\d{1,3}\\.\\d{1,3}|127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}|0:0:0:0:0:0:0:1|::1";
private String protocolHeader;
private String protocolHeaderHttpsValue = "https";
private String hostHeader = "X-Forwarded-Host";
private String portHeader = "X-Forwarded-Port";
private String remoteIpHeader;
public Remoteip() {
}
第二点:需要给请求头中的X-Forwarded-Proto设置合适的值
if (this.protocolHeader != null) {
//获取X-Forwarded-Proto的值
hostHeaderValue = request.getHeader(this.protocolHeader);
if (hostHeaderValue != null) {
//判断是否是https请求
if (this.isForwardedProtoHeaderValueSecure(hostHeaderValue)) {
//判断是https,改写request请求信息
request.setSecure(true);
request.getCoyoteRequest().scheme().setString("https");
//设置端口
this.setPorts(request, this.httpsServerPort);
} else {
//判断是http,改写request请求信息
request.setSecure(false);
request.getCoyoteRequest().scheme().setString("http");
this.setPorts(request, this.httpServerPort);
}
}
}
这里就是在前面为什么nginx请求要配置X-Forwarded-Proto值的原因,只有这样才能获取到hostHeaderValue的值,才能对request请求信息进行修改。
这里需要注意的是,判断是否是https请求和设置端口这两步,都是和Remoteip中的配置属性进行比较,在isForwardedProtoHeaderValueSecure方法中,用请求头X-Forwarded-Proto的值,来和Remoteip中的protocolHeaderHttpsValue属性来比较,这个值其实默认是https,但是如果像上面一样把这个值在配置文件改写成了http,那代表即使hostHeaderValue的值设置成http,也能改写成https请求。
roxy_set_header X-Forwarded-Proto http;
同理setPorts方法,也可以通过Remoteip的httpServerPort属性,也就是请求头的X-Forwarded-Port属性,来设置自定义端口,如果不设置,https默认使用443,http默认使用80。
所以才有nginx的设置:
proxy_set_header X-Forwarded-Port 8080;
RemoteIpValve何时加入tomcat容器
知道了RemoteIpValve的执行原理,也了解了tomcat何时执行这个方法,那么Springboot是如何把RemoteIpValve加入到tomcat容器中作为首选引擎的呢?
RemoteIpValve引擎又是如何通过tomcat的简单配置,就可以自动开启的呢?
带着这两个问题,我们进入RemoteIpValve的加载方法。
tomcat开启RemoteIpValve引擎
通过RemoteIpValve的setProtocolHeader方法,我们可以看到,RemoteIpValve的加载是在tomcatServletWebServerFactory这个beanName实例化的过程中,在执行方法initializeBean的过程中,执行bean的后置处理器——applyBeanPostProcessorsBeforeInitialization,通过加载容器中实现了接口WebServerFactoryCustomizer的自定义器集合,逐个执行。
@FunctionalInterface
public interface WebServerFactoryCustomizer<T extends WebServerFactory> {
void customize(T factory);
}
在执行到容器中的WebServerFactoryCustomizer的实现类TomcatWebServerFactoryCustomizer的customize方法时,执行了this.customizeRemoteIpValve(factory)方法,借此来通过tomcat配置开启流程引擎——RemoteIpValve:
private void customizeRemoteIpValve(ConfigurableTomcatWebServerFactory factory) {
//获取tomcat下配置的remoteip属性
Remoteip remoteIpProperties = this.serverProperties.getTomcat().getRemoteip();
String protocolHeader = remoteIpProperties.getProtocolHeader();
String remoteIpHeader = remoteIpProperties.getRemoteIpHeader();
//如果protocolHeader或者remoteIpHeader不为空,开启RemoteIpValve引擎配置
//通常来说就是请求头参数X-Forwarded-Proto或者X-Forwarded-For
if (StringUtils.hasText(protocolHeader) || StringUtils.hasText(remoteIpHeader) || this.getOrDeduceUseForwardHeaders()) {
RemoteIpValve valve = new RemoteIpValve();
valve.setProtocolHeader(StringUtils.hasLength(protocolHeader) ? protocolHeader : "X-Forwarded-Proto");
if (StringUtils.hasLength(remoteIpHeader)) {
valve.setRemoteIpHeader(remoteIpHeader);
}
valve.setInternalProxies(remoteIpProperties.getInternalProxies());
try {
valve.setHostHeader(remoteIpProperties.getHostHeader());
} catch (NoSuchMethodError var7) {
}
valve.setPortHeader(remoteIpProperties.getPortHeader());
valve.setProtocolHeaderHttpsValue(remoteIpProperties.getProtocolHeaderHttpsValue());
//tomcat加入流程引擎
factory.addEngineValves(new Valve[]{valve});
}
}
都是很简单的get/set代码,结合Remoteip配置看会更清晰一些。
RemoteIpValve引擎加入tomcat
这里,我们说重点——RemoteIpValve引擎怎么加入tomcat容器:
factory.addEngineValves(new Valve[]{valve});
首先看这个,其实很简单只是单纯的把RemoteIpValve加入——TomcatServletWebServerFactory中的一个集合属性engineValves中。
那么重点便是engineValves是如何加入tomcat的?
可以通过类中的configureEngine方法断点来查看:
private void configureEngine(Engine engine) {
engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay);
Iterator var2 = this.engineValves.iterator();
while(var2.hasNext()) {
Valve valve = (Valve)var2.next();
//给tomcat管道添加引擎
engine.getPipeline().addValve(valve);
}
}
这里可以清晰的看到,就是在Springboot启动过程中,调用到AbstractApplicationContext的refresh方法中的onRefresh的时候,创建了一个tomcat服务器,通过getWebServer这个方法来配置tomcat启动的一系列属性:
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
......省略不重要内容
//配置流程引擎
this.configureEngine(tomcat.getEngine());
......
return this.getTomcatWebServer(tomcat);
}
小结
通过添加tomcat的自定义引擎——RemoteIpValve,我们从根本上了解了tomcat下remoteip配置的底层原理,希望对大家有所帮助。