下午组内同事反馈,经过IoGateway(基于Spring Cloud Gateway开发)服务转发的请求后端服务无法正确获取query中的参数,具体表现为后端服务获取到的query参数已经被编码,同事初步怀疑是IoGateway对请求的URL进行了编码,导致已经编码过的请求经过IoGateway后再次被编码,因而后端服务拿到的参数是编码后的参数,事实证明这位同事是正确的,为这位同事点赞。对同事描述的问题可以简单的抽象为:client发送的请求中的URL已经被编码,经过IoGateway后请求中的URL又一次被编码。一个简单的例子:URL http://iogateway-domain-name/api/app?appId=app%24id,请求经过IoGateway转发后变为:http://iogateway-domain-name/api/app?appId=app%2524id,这时后端服务获取到的查询参数appId的值为app%24id
,导致后端服务无法开展业务。IoGateway和其他服务的部署视图如下,定位过程就是一步一步确定引入问题的服务,使用的当然是伟大的算法:排除法:

其中ELB使用的是华为云提供云服务,没有办法对其操作或者抓包,尤其是ELB收到的包。
定位过程
-
1、首先查看了Spring Cloud Gateway关于URL编码的issue和StackOverflow上面的问题,Spring Cloud Gateway确实在之前的版本出现过URL被二次编码的问题,但是issue已经被关闭,而且解决issue的代码已经被merge并且随2.2.4.RELEASE版本发布,IoGateway使用的版本为2.2.5.RELEASE。定位结论 :问题可能不是由开源组件引入。
-
2、在初步排除了Spring Cloud Gateway的问题后,通过编写测试代码在本地进行测试,测试结果表明IoGateway没有对URL二次编码。定位结论 不是IoGateway的问题,可能是Nginx对请求的URL进行了编码。定位过程存在的问题 :测试使用的Route经过的Filter和实际测试环境的Route经过的Filter不同,这一点没有早点意识到,导致在错误的方向上渐行渐远。
-
3、怀疑是Nginx的问题后,通过ELB直接发起相同的请求,后端服务能够正确获取参数,这表明Nginx没有对请求进行编码。当时内心是痛苦的,难道是ELB,这不太可能。难道测试场景IoGateway发送到ELB的请求为HTTP协议,实际情况为HTTPS协议,是HTTPS协议带来的问题嘛?虽然我怀疑可能是HTTPS协议带来的问题,但是一时也没有办法验证,决定从其他方面看看。定位结论 :不是Nginx的问题,问题还是出在IoGateway的身上。
-
4、既然定位到是Spring Cloud Gateway对URL进行了二次编码,下面的目标就是确定在什么位置对请求进行了编码。请求在Spring Cloud Gateway中经过GatewayFilter和GlobalFilter后转发到后端,第一时间想到的是确定Spring Cloud Gateway转发出去的请求是什么样子的。请求在Spring Cloud Gateway中经过的最后一个Filter为
NettyRoutingFilter
,该Filter的主要功能就是使用reactor-netty的client将请求转发到后端服务,经过定位发现请求的URL被NettyRoutingFilter
转发时已经被二次编码过,因此可以断定,请求的URL在Spring Cloud Gateway内部二次编码了。定位结论:Spring Cloud Gateway的Filter编码了URL。 -
5、定位到是Spring Cloud Gateway的Filter将请求的URL二次编码后,仔细想了一下Spring Cloud Gateway的默认Filter实现不会干这个事情(不然早就被提出了),那么只剩下一种可能那就是自定义的Filter编码了URL。经过一个个的排查发现,一个自定义实现负载均衡算法的Filter会重新构建URL,重建URL的方法实现如下:
public URI rebuildURI(URI originalUri, String host, String port) { return UriComponentsBuilder.fromUri(originalUri) .host(host) .port(port) .build().toUri(); }
rebuildURI方法终究错在哪里?
既然已经确定就是这个rebuildURI
方法对URL进行了编码,接下来只有两件事情在等我们:修改方法以确保正确工作和弄懂为什么错误。修改方法很简单,在调用build()方法时传入参数表明URL是否已经被编码过即可,修改后的方法如下:
public static URI rebuildURI(URI originalUri, String host, int port) {
boolean encoded = ServerWebExchangeUtils.containsEncodedParts(originalUri);
return UriComponentsBuilder.fromUri(originalUri)
.host(host)
.port(port)
.build(encoded).toUri();
}
问题的关键就在build()
和build(boolean encoded)
两个函数的区别,build()
函数的实现为build(false)
,也就是说明当前的URL没有被完全编码,那么UriComponentsBuilder
内部将会对URL进行完全编码,导致已经编码过一次的URL被第二次编码,这也是遇到问题的根本原因。
从这个问题得到的启示
代码一定要有完备UT,如果自定义的Filter有完全的UT覆盖,这个问题很早就可以发现,不至于遗留到后端导致解决的成本增高。
网友评论