Spring Cloud实战小贴士:Zuul处理Cookie和重定向

由于我们在之前所有的入门教程中,对于HTTP请求都采用了简单的接口实现。而实际使用过程中,我们的HTTP请求要复杂的多,比如当我们将Spring Cloud Zuul作为API网关接入网站类应用时,往往都会碰到下面这两个非常常见的问题:

- 会话无法保持

- 重定向后的HOST错误

本文将帮助大家分析问题原因并给出解决这两个常见问题的方法。

会话保持问题

通过跟踪一个HTTP请求经过Zuul到具体服务,再到返回结果的全过程。我们很容易就能发现,在传递的过程中,HTTP请求头信息中的Cookie和Authorization都没有被正确地传递给具体服务,所以最终导致会话状态没有得到保持的现象。

那么这些信息是在哪里丢失的呢?我们从Zuul进行路由转发的过滤器作为起点,来一探究竟。下面是RibbonRoutingFilter过滤器的实现片段:

public class RibbonRoutingFilter extends ZuulFilter{

	...

	protected ProxyRequestHelper helper;

	

	@Override

	public Object run() {

		RequestContext context = RequestContext.getCurrentContext();

		this.helper.addIgnoredHeaders();

		try {

			RibbonCommandContext commandContext = buildCommandContext(context);

			ClientHttpResponse response = forward(commandContext);

			setResponse(response);

			return response;

		}

		...

		return null;

	}

	

		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);

		...

	}

}

这里有三个重要元素:

  • 过滤器的核心逻辑run函数实现,其中调用了内部函数buildCommandContext来构建上下文内容
  • buildCommandContext中调用了helper对象的buildZuulRequestHeaders方法来处理请求头信息
  • helper对象是ProxyRequestHelper类的实例

接下来我们再看看ProxyRequestHelper的实现:

public class ProxyRequestHelper {

	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;

	}

	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;

			}

		}

		...

	}

}

从上述源码中,我们可以看到构建头信息的方法buildZuulRequestHeaders通过isIncludedHeader函数来判断当前请求的各个头信息是否在忽略的头信息清单中,如果是的话就不组织到此次转发的请求中去。那么这些需要忽略的头信息是在哪里初始化的呢?在PRE阶段的PreDecorationFilter过滤器中,我们可以找到答案:

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块,通过调用ProxyRequestHelperaddIgnoredHeaders方法来添加需要忽略的信息到请求上下文中,供后续ROUTE阶段的过滤器使用。这里的if/else块分别用来处理全局设置的敏感头信息和指定路由设置的敏感头信息。而全局的敏感头信息定义于ZuulProperties中:

@Data

@ConfigurationProperties("zuul")

public class ZuulProperties{

	private Set<String> sensitiveHeaders = new LinkedHashSet<>(

			Arrays.asList("Cookie", "Set-Cookie", "Authorization"));

	...

}

所以解决该问题的思路也很简单,我们只需要通过设置sensitiveHeaders即可,设置方法分为两种:

  • 全局设置:
    • zuul.sensitive-headers=
  • 指定路由设置:
    • zuul.routes.<routeName>.sensitive-headers=
    • zuul.routes.<routeName>.custom-sensitive-headers=true

重定向问题

在使用Spring Cloud Zuul对接Web网站的时候,处理完了会话控制问题之后。往往我们还会碰到如下图所示的问题,我们在浏览器中通过Zuul发起了登录请求,该请求会被路由到某WebSite服务,该服务在完成了登录处理之后,会进行重定向到某个主页或欢迎页面。此时,仔细的开发者会发现,在登录完成之后,我们浏览器中URL的HOST部分发生的改变,该地址变成了具体WebSite服务的地址了。这就是在这一节,我们将分析和解决的重定向问题!

出现该问题的根源是Spring Cloud Zuul没有正确的处理HTTP请求头信息中的Host导致。在Brixton版本中,Spring Cloud Zuul的PreDecorationFilter过滤器实现时完全没有考虑这一问题,它更多的定位于REST API的网关。所以如果要在Brixton版本中增加这一特性就相对较为复杂,不过好在Camden版本之后,Spring Cloud Netflix 1.2.x版本的Zuul增强了该功能,我们只需要通过配置属性zuul.add-host-header=true就能让原本有问题的重定向操作得到正确的处理。关于更多Host头信息的处理,读者可以参考本文之前的分析思路,可以通过查看PreDecorationFilter过滤器的源码来详细更多实现细节。

原文发布于微信公众号 - 程序猿DD(didispace)

原文发表时间:2017-05-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏芋道源码1024

熔断器 Hystrix 源码解析 —— 命令合并执行

本文主要基于 Hystrix 1.5.X 版本 1. 概述 2. HystrixCollapser 2.1 构造方法 2.2 执行命令方式 2.3 核心方法 3...

3817
来自专栏Java后端技术

Java发送邮件初窥

  最近朋友的公司有用到这个功能,之前对这一块也不是很熟悉,就和他一起解决出现的异常的同时,也初窥一下使用Apache Common Email组件进行邮件发送...

1502
来自专栏岑玉海

hbase源码系列(十)HLog与日志恢复

HLog概述 hbase在写入数据之前会先写入MemStore,成功了再写入HLog,当MemStore的数据丢失的时候,还可以用HLog的数据来进行恢复,下面...

4048
来自专栏程序猿DD

Edgware.RC1中ZuulFallbackProvider的改进

作者:ligang 原文:http://www.spring4all.com/article/158 如何在Zuul中使用fallback功能 我们在项目中使用...

2489
来自专栏雪胖纸的玩蛇日常

第十九章 Django的ORM映射机制

2066
来自专栏生信宝典

分子对接简明教程 (二)

用PyMOL展示配体和受体相互作用的原子和氢键 为了简化展示过程,我们设计了一个pml脚本 (脚本内有很详细的解释),只需要修改脚本里面受体和配体的名字,然后在...

5105
来自专栏开发之途

Android 模拟登陆网站实现移动客户端

5848
来自专栏生信宝典

简单可视化-送你一双发现美的眼睛

用PyMOL展示配体和受体相互作用的原子和氢键 为了简化展示过程,我们设计了一个pml脚本 (脚本内有很详细的解释),只需要修改脚本里面受体和配体的名字,然后在...

2566
来自专栏小李刀刀的专栏

[译]Laravel 5.0 之命令及处理程序

本文译自 Matt Stauffer 的系列文章. ---- 本文中涉及的新功能都是关于 Commands 的,这些特性在 Laravel 旧版本中已经有了,但...

3646
来自专栏阿杜的世界

Java Web技术经验总结(十七)

882

扫码关注云+社区

领取腾讯云代金券