SAML message intended destination endpoint did not match recipient endpoint,问题解决及原理

5 篇文章 1 订阅

目录

问题背景

问题解决

端口不匹配

解决方法一(tomcat老版本):

解决方法二(tomcat新版本):

http请求方式不匹配

解决方法一(tomcat老版本):

解决方法二(tomcat新版本,生产推荐):

解决方法三(tomcat新版本,nginx只能配置http):

原理解析

RemoteIpValve重点

第一点:需要把当前请求中的ip地址是可信任ip

第二点:需要给请求头中的X-Forwarded-Proto设置合适的值

RemoteIpValve何时加入tomcat容器

tomcat开启RemoteIpValve引擎

RemoteIpValve引擎加入tomcat

小结


问题背景

在使用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配置的底层原理,希望对大家有所帮助。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值