Spring Cloud Zuul 会话保持问题

问题描述:

通过zuul智能路由到后端服务,其中有个服务是需要通过Basic Auth认证

配置路由没问题,但请求返回始终是401,认证没通过

原因查找

查看请求参数,发现Header信息通过Zuul后丢失

问题分析

Zuul默认过滤掉请求Header,一定有对Header过滤的配置

解决问题

#保留请求header信息
zuul.routes.xxx.sensitive-headers=Authorization,sign,sign-type
zuul.routes.xxx.custom-sensitive-headers=true

会话保持问题

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

那么这些信息是在哪里丢失的呢?我们从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

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/04/01/spring-cloud-zuul-session-retention-issues/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Spring Cloud Zuul 会话保持问题
问题描述: 通过zuul智能路由到后端服务,其中有个服务是需要通过Basic Auth认证 配置路由没问题,但请求返回始终是401,认证没通过 原因查找 查看请求参数,……
<<上一篇
下一篇>>
文章目录
关闭
目 录