【技巧】zuul代理之后响应Header重复的问题解决

文章探讨了一个Zuul网关代理后静态资源访问的问题,即响应头出现重复Vary键值对。提供了两种解决方案:一是网关层面忽略特定Header;二是后端服务层通过HandlerInterceptor移除VaryHeader。文章还分析了问题产生的原因和源码执行流程。
摘要由CSDN通过智能技术生成

1. 问题表现

一个提供静态文件访问的后台服务,在被以Zuul1.x为基础搭建的gateway应用代理之后,访问类似http://{outIp}:{outPort}/{serviceName}/xxx/thumb/1225.map_01.png的静态资源时,响应头里会出现重复的Vary键值对:
在这里插入图片描述

系统之间的交互流程图如下:

浏览器 Zuul网关 上游/后台服务 http://{outIp}:{outPort}/{serviceName}/xxx/thumb/1225.map_01.png http://{innerIp}:{innerPort}/xxx/thumb/1225.map_01.png 1225.map_01.png 1225.map_01.png 浏览器 Zuul网关 上游/后台服务

2. 解决方案(二选一)

方案一(推荐)

在网关层解决。

zuul增加如下配置:

# 要完全忽略的HTTP标头的名称(即将它们排除在下游请求之外,并将它们从下游响应中删除)。 意思是: 既会从转发给下游的服务请求中删除该header; 也会在下游回复的响应中, 在转发回客户端/浏览器端时删除掉. 
# 对应源码: ProxyRequestHelper.isIncludedHeader()
# 生效位置参见 ProxyRequestHelper.isIncludedHeader() 的调用.
zuul:
  ignored-headers: Vary, Access-Control-Allow-Origin, X-Forwarded-For

优缺点:

  1. 方便快捷,无需修改代码。
  2. 对header的操作不够精细。以这里的"Vary" Header为例,如果自定义业务逻辑里又需要传递特定的Vary键值对,那么很明显这里是无法适应的。
方案二

在上游/后端服务层解决。

增加如下一个HandlerInterceptor实现类。


##### 方法二
/**
 * <p>
 * 对于静态文件访问, SpringMVC源码层面采用{@code SimpleUrlHandlerMapping + ResourceHttpRequestHandler}进行处理
 * <p>
 * 在{@code SimpleUrlHandlerMapping}的基类{@code AbstractHandlerMapping}中存在逻辑"如果发现handler实现了
 * {@code CorsConfigurationSource}"接口就会启用CORS设置 —— 参见 {@code AbstractHandlerMapping.CorsInterceptor}的设置
 * <p>
 * 负责处理静态文件查找的{@code ResourceHttpRequestHandler}恰好处于这一逻辑链条上.
 * <p> 
 * <p>
 * 因此即使在微服务模式下没有启用CORS配置, 对于上游/后端服务中静态文件的访问依然会出现重复的Vary Http header.
 * 
 * <p>
 * 本类意图缓解这一问题 —— 在请求返回前检查Vary Http Header, 存在就删除.
 * <p>
 * 注: 这个方式并不完美. 存在逻辑反复的嫌疑。
 * <p>
 * 
 * @see {@code AbstractHandlerMapping.CorsInterceptor} ; {@code DefaultCorsProcessor}
 * 
 */
@Configuration
public class CorsHeaderRemoverInterceptorConfig extends HandlerInterceptorAdapter implements WebMvcConfigurer {
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
        ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);

        Collection<String> varyHeaders = response.getHeaders(HttpHeaders.VARY);
        if (CollUtil.isNotEmpty(varyHeaders)) {
            response.setHeader(HttpHeaders.VARY, "");
        }
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CorsHeaderRemoverInterceptorConfig());
    }
}

优缺点:

  1. 能够解决方案一中需要保留特定"Vary "Header的问题。
  2. 脱离对于指定技术栈的依赖(比如上面方案一里的zuul)。
  3. 一定的复杂性,以及逻辑反复的重复感觉(SpringMVC给加上,然后你又给移除了)。

3. 问题产生原因分析

3.1 根因定位(基于Arthas)
# 确定后端服务的返回中存在"Vary"响应Header
 watch org.springframework.web.servlet.DispatcherServlet doDispatch '{params[1].getHeaderNames(),params[1].getHeader("Vary")}' -x 2 -n 1 'params[0].getRequestURI().contains("/1225.map_01.png")'

# 定位"Vary"响应Header是在哪里被添加的, 打印出调用堆栈, 定位最终原因
 watch javax.servlet.http.HttpServletResponse addHeader '{params, @java.lang.Thread@currentThread().getStackTrace()}' -x 2 -n 1 'params[0] == "Vary"'


# =============================================================================
#              Arthas辅助表达式
# =============================================================================
# 处理当前请求的handler是否支持进行CORS配置
# 当handler为处理静态资源响应的ResourceHttpRequestHandler时, 该方法返回true. (因为ResourceHttpRequestHandler实现了CorsConfigurationSource接口)
watch org.springframework.web.servlet.handler.AbstractHandlerMapping hasCorsConfigurationSource '{params, returnObj, target}' -x 2 -n 5 'returnObj == true'

# 确认处理当前请求的ResourceHttpRequestHandler是否有额外的CORS配置
vmtool -x 3 --action getInstances --className  org.springframework.web.servlet.resource.ResourceHttpRequestHandler  --express 'instances[6].corsConfiguration'

# 确认针对当前请求, Zuul是否抛弃了特定响应header
watch org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper isIncludedHeader '{params[0], returnObj}' -n 5 'params[0] == "Vary" and @com.netflix.zuul.context.RequestContext@getCurrentContext().getRequest().getRequestURI().contains("/1225.map_01.png")'
3.2 源码执行流程分析

通过条件断点调试,最终可以得到如下堆栈图:
在这里插入图片描述

最终梳理之下:

  1. 负责处理静态文件响应的HandlerMapping实现类为SimpleUrlHandlerMapping。而相应进行实际静态文件查找读取的handler则是ResourceHttpRequestHandler
  2. ResourceHttpRequestHandler实现了接口CorsConfigurationSource,这呼应了作为SimpleUrlHandlerMapping继承自基类AbstractHandlerMapping中的hasCorsConfigurationSource方法 —— 判断当前handler(这里是ResourceHttpRequestHandler)是否继承自CorsConfigurationSource
    // AbstractHandlerMapping.java (SimpleUrlHandlerMapping的基类)
	@Override
	@Nullable
	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		Object handler = getHandlerInternal(request);
		......

		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
		......

		// 对于ResourceHttpRequestHandler, 这里的hasCorsConfigurationSource(handler)返回true
		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			config = (config != null ? config.combine(handlerConfig) : handlerConfig);
			// 这里会向请求流程的拦截器集合的头部插入一个AbstractHandlerMapping.CorsInterceptor实例,以实现在请求正式处理前的CORS设置。
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}

		return executionChain;
	}

4. 相关

  1. zuul跨域配置报错分析
  2. Arthas官方文档 - 命令watch
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值