目录:
SpringCloud基础(0)
SpringCloud基础(1)
SpringCloud基础(2)
SpringCloud基础(3)
SpringCloud基础(4)
SpringCloud基础(5)
8 Zuul处理Cookie和重定向
在实际项目中,将Spring Cloud Zuul作为API网关接入时,经常会碰到下面两个常见的问题:
会话无法保持
重定向后的HOST错误
8.1 会话保持问题
通过跟踪一个HTTP请求经过Zuul到具体服务,再到返回结果的全过程。可以发现在传递的过程中,HTTP请求头信息中的Cookie和Authorization都没有被正确传递给具体服务,所以最终导致会话状态没有得到保持的现象。
请求一进来,首先会经过Zuul的路由转发过滤器,RibbonRoutingFilter过滤器run()函数如下:
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders(new String[0]);
try {
RibbonCommandContext commandContext = this.buildCommandContext(context);
ClientHttpResponse response = this.forward(commandContext);
this.setResponse(response);
return response;
} catch (ZuulException var4) {
throw new ZuulRuntimeException(var4);
} catch (Exception var5) {
throw new ZuulRuntimeException(var5);
}
}
可以看到,过滤函数获取到请求内容之后,通过this.buildCommandContext(context)进行处理,返回上下文内容。而this.buildCommandContext(context)函数实现如下:
protected RibbonCommandContext buildCommandContext(RequestContext context) {
HttpServletRequest request = context.getRequest();
MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
…
return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params,
requestEntity, this.requestCustomizers, contentLength);
}
该函数通过this.helper.buildZuulRequestHeaders(request)处理请求头信息,helper对象是
ProxyReuqestHelper类的实例,其中buildZuulRequestHeaders(request)如下:
public MultiValueMap<String, String> buildZuulRequestHeaders(
HttpServletRequest request) {
RequestContext context = RequestContext.getCurrentContext();
MultiValueMap<String, String> headers = new HttpHeaders();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
if (isIncludedHeader(name)) {
Enumeration<String> values = request.getHeaders(name);
while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}
}
}
Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders();
for (String header : zuulRequestHeaders.keySet()) {
headers.set(header, zuulRequestHeaders.get(header));
}
headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
return headers;
}
可以看到,构建头信息时通过isIncludeHeader函数判断当前请求的各个头信息是否在忽略的头信息中,如果是的话就不加入到转发headers中去:
public boolean isIncludedHeader(String headerName) {
String name = headerName.toLowerCase();
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey(IGNORED_HEADERS)) {
Object object = ctx.get(IGNORED_HEADERS);
if (object instanceof Collection && ((Collection<?>) object).contains(name)) {
return false;
}
}
switch (name) {
case "host":
case "connection":
case "content-length":
case "content-encoding":
case "server":
case "transfer-encoding":
case "x-application-context":
return false;
default:
return true;
}
}
需要呼略的头信息就是此处的IGNORED_HEADERS=“ignoredHeaders”,该常量的初始化位置为PreDecorationFilter的run()函数:
public class PreDecorationFilter extends ZuulFilter {
...
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId());
// 处理忽略头信息的部分
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders(
this.properties.getSensitiveHeaders()
.toArray(new String[0]));
} else {
this.proxyRequestHelper.addIgnoredHeaders(
route.getSensitiveHeaders()
.toArray(new String[0]));
}
...
}
可以看到,注释的地方有个if/else块。在该块中,通过判断是否存在自定义的全局敏感信息头,如不存在就通过addIgnoredHeaders方法将默认的敏感信息头添加到忽略头信息中;如果有自定义全局敏感信息头,就将其加入到呼略头信息中。而默认的敏感信息头在定义于ZuulProperties properties中:
@Data
@ConfigurationProperties("zuul")
public class ZuulProperties {
private Set<String> sensitiveHeaders = new LinkedHashSet<>(
Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
...
}
至此会话无法保持的问题就很清楚了,就是因为没有设置全局敏感头信息,从而zuul将默认的敏感头信息添加到转发忽略头中去了,而cookie刚好包含在默认的全局敏感头信息中。解决办法就是设置自定义敏感头信息,设置方法有两种,一种是设置全局敏感头信息:
zuul.sensitive-headers=xxx
另一种是指定路由设置:
zuul.routes..sensitive-headers=xxx
zuul.routes..custom-sensitive-headers=true
8.2 重定向问题
在使用Spring Cloud Zuul时往往还会碰到以下问题,即在浏览器中通过zuul发起登录请求,该请求会被路由到某website服务,该服务在完成了登录处理后,进行重定向到某个主页或欢迎页面。此时在登录完成之后,浏览器中url的host部分会发生改变,该地址变成了具体的website服务的地址了。
出现该问题的原因是Spring Cloud Zuul没有正确处理HTTP请求头信息中的host导致。在Brixton之后的版本中,通过配置属性zuul.add-host-header=true就能让重定向操作得到正确的处理。
原文:http://blog.didispace.com/spring-cloud-zuul-cookie-redirect/
#9 Zuul过滤器
前文中说过,实现ZuulFilter接口的过滤器需要实现四个方法,其中一个是filterType(),该方法返回一个字符串,代表过滤器类型,分别是:pre,route,post,error。
需要注意的是,当使用的注解不同时,开启的过滤器也是不同的。常用的注解是@EnableZuulProxy,可以理解为@EnableZuulServer的增强版。在官方文档中,@EnableZuulServer是一个“空白”的zuul,它包含以下过滤器:
pre类型的过滤器有ServletDetectionFilter,FormBodyWrapperFilter和DebuggerFilter。
ServletDetectionFilter过滤器用于检查请求是否通过Spring Dispatcher,然后通过isDispatcherServerletRequest设置布尔值。
FormBodyWrapperFilter过滤器用于解析表单数据,并为请求重新编码。
DebuggerFilter过滤器顾名思义是调试用的过滤器,可以通过设置zuul.debug.request=true或者在请求时在请求参数中加上debug=true来开启。开启之后过滤器会把RequestContext.setDebugRouting()和RequestContext.setDebugRequest()设置true。
route类型的过滤器有sendForwardFilter,该过滤器使用Servlet RequestDispathcer转发请求,转发位置存储在RequestContext.getCurrentContext().get(“forward.to”)中。
post类型过滤器有SendResponseFilter,它将zuul所代理的微服务的响应写入当前响应。
error类型过滤器有SendErrorFilter,该过滤器判断RequestContext.getThrowable()是否为空,若不为空,也就是说处理请求过程中发生了错误,此时默认转发到/error。当然也可以设置error.path属性修改默认的转发路径。
而当使用**@EnableZuulProxy**注解时,除上述过滤器外,Spring Cloud还会安装以下过滤器:
pre类型过滤器
PreDecorationFilter:该过滤器根据提供的RouteLocator确定路由到的地址,以及怎样去路由。该过滤器也可为后端请求设置各种代理相关的header,在前文8.1会话保持问题中,忽略头ignoredHeaders的初始化就是在该过滤器中实现的。
route类型过滤器
RibbonRoutingFilter过滤器使用Ribbon,Hystrix和可插拔的HTTP客户端发送请求。该过滤器可使用不同HTTP客户端,默认使用Apache HttpClient。该过滤器会对后端请求的header和参数进行处理。
SimpleHostRoutingFilter过滤器通过Apache HttpClient向指定的URL发送请求,URL在RequestContext.getRouteHost中。