使用SpringCloud Zuul过程中遇到的问题

背景

使用SpringCloud Zuul开发一个可后台配置的网关系统,用户将转发规则写入数据库,Zuul网关启动的时候去数据库加载路由规则,进行请求转发。

Zuul对接的后台服务不涉及服务注册与发现组件,从API接口捞取后台服务列表和地址,网关只需要根据规则将请求指向后台其中一台即可。采用Zuul该搭建该系统的原因在于,Zuul的转发原理可以让我们很方便的对请求的内容就行包装和处理。而基于Nginx做开发成本太高,涉及的编程语言门槛和其他一些因素。

该网关系统采用@EnableZuulServer进行注解,无须涉及RibbonRoutingFilter。转发过程实际只需要根据规则将请求指向后台特定IP机器即可,因此该系统的route过滤器与SimpleHostRoutingFilter非常类似,完全可以基于SimpleHostRoutingFilter的源码做订制开发,从而来满足我们这个需求。

请求URL编码问题

通过网关发起的Get请求,到后台服务器时,上送的参数会被强制执行URLEncode。

在SimpleHostRoutingFilter过滤器中,有一个参数forceOriginalQueryStringEncoding,顾名思义,强制采用原始请求的编码格式,即不对Get请求参数做编解码。SimpleHostRoutingFilter过滤器中构造HttpRequest的方法如下,

构建HttpRequest
1
2
3
4
5
6
7
8
protected HttpRequest buildHttpRequest(String verb, String uri,
InputStreamEntity entity, MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, HttpServletRequest request) {
HttpRequest httpRequest;
String uriWithQueryString = uri + (this.forceOriginalQueryStringEncoding
? getEncodedQueryString(request) : this.helper.getQueryString(params));
...
}

该方法在构建url的时候,会根据forceOriginalQueryStringEncoding这个值执行不同的操作,当为true的时候,调用getEncodedQueryString方法,如下图所示,

获取QueryString
1
2
3
4
5
private String getEncodedQueryString(HttpServletRequest request) {
String query = request.getQueryString();
return (query != null) ? "?" + query : "";
}
}

可以看到,getEncodedQueryString方法直接返回request.QueryString,将不会对请求参数进行编解码操作。而this.helper.getQueryString方法会对请求参数做编解码处理。因此,只需要将forceOriginalQueryStringEncoding设置为true即可。该参数可以在Zuul的properties属性中配置,该参数在构造SimpleHostRoutingFilter中初始化的时候,是通过调用properties.isForceOriginalQueryStringEncoding()方法来进行初始化的。

302重定向问题

使用HttpClient进行通讯的过程中,如果遇到302、301这一类的重定向,HttpClient会自动发起二次请求,而这个时候的请求源IP则变成了网关地址,会造成IP漂移需要重新登录,因此需要将HttpClient的自动重定向禁用。将HttpClient的初始化改成HttpClientBuilder.create().disableRedirectHandling().build();增加一个disableRedirectHandling方法即可。

请求Body编码问题

这个问题源于Android客户端登陆时候遇到的,使用IPhone客户端登陆成功,但是Android客户端登陆失败。通过跟踪发现,Android客户端通过Post请求上送数据的时候,ContentType为application/x-www-form-urlencoded,而body中的数据为JSON格式。网关在处理的时候,经过FormBodyWrapperFilter会对数据进行编码,将body中的JSON格式编码转换,因此后台服务收到的数据是不正确的。IPhone之所以能登陆成功,是因为ContentType传的是application/json,FormBodyWrapperFilter的ShouldFilter方法会自动跳过这种情况。为了兼容已经在生产的客户端版本,网关采用禁用该过滤器的方式来解决该问题。通过在application.properites文件中增加zuul.FormBodyWrapperFilter.pre.disable=true配置。

X-Forward-For与Host

采用@EnableZuulServer和@EnableZuulProxy的一个区别就是,当使用@EnableZuulProxy作为注解的时候,会执行PreDecorationFilter过滤器,在该过滤器中会对请求头部做一些处理,比如带上以下参数:

FilterConstants.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* X-Forwarded-For Header
*/
public static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For";
/**
* X-Forwarded-Host Header
*/
public static final String X_FORWARDED_HOST_HEADER = "X-Forwarded-Host";
/**
* X-Forwarded-Prefix Header
*/
public static final String X_FORWARDED_PREFIX_HEADER = "X-Forwarded-Prefix";
/**
* X-Forwarded-Port Header
*/
public static final String X_FORWARDED_PORT_HEADER = "X-Forwarded-Port";
/**
* X-Forwarded-Proto Header
*/
public static final String X_FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";

而在ZuulProperites类中可以看到,addProxyHeaders默认为true,addHostHeader默认为false。所以采用@EnableZuulProxy作为注解的时候,会带上Forwarded相关头部信息,不会带上Host头部信息。
ZuulProperites.java
1
2
3
4
5
6
7
8
9
   /**
* Flag to determine whether the proxy adds X-Forwarded-* headers.
*/
private boolean addProxyHeaders = true;

/**
* Flag to determine whether the proxy forwards the Host header.
*/
private boolean addHostHeader = false;

而本网关高度订制化,采用@EnableZuulServer作为注解,因此需要手动对所需Forward相关参数和Host进行处理。

IIS Request.Url 带端口号的问题

由于网关后台对接了asp.net的IIS服务,使用了.net中的Request.Url.AbsoluteUri方法来取值,这个方法会自动拼接Host和端口。网关在测试环境中将会遇到问题,例如测试环境是网关地址99.6.140.1:808,而后台.net服务器地址是99.6.163.3:888。即使在网关中将Host设置为99.6.140.1:808,通过上述方法取得的值还是,99.6.163.3:808,自动拼接上了.net目的地址的端口。此问题目前还未找到解决方案,网关无法解决,只能通过.net后台服务进行修改。

Transfer-Encoding:chunked 与 Content-Length

Content-Encoding:Gzip 自动解压

httpclient disableContentCompress