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

系统之间的交互流程图如下:
2. 解决方案(二选一)
方案一(推荐)
在网关层解决。
zuul增加如下配置:
# 要完全忽略的HTTP标头的名称(即将它们排除在下游请求之外,并将它们从下游响应中删除)。 意思是: 既会从转发给下游的服务请求中删除该header; 也会在下游回复的响应中, 在转发回客户端/浏览器端时删除掉.
# 对应源码: ProxyRequestHelper.isIncludedHeader()
# 生效位置参见 ProxyRequestHelper.isIncludedHeader() 的调用.
zuul:
ignored-headers: Vary, Access-Control-Allow-Origin, X-Forwarded-For
优缺点:
- 方便快捷,无需修改代码。
- 对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());
}
}
优缺点:
- 能够解决方案一中需要保留特定"Vary "Header的问题。
- 脱离对于指定技术栈的依赖(比如上面方案一里的zuul)。
- 一定的复杂性,以及逻辑反复的重复感觉(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 源码执行流程分析
通过条件断点调试,最终可以得到如下堆栈图:

最终梳理之下:
- 负责处理静态文件响应的
HandlerMapping实现类为SimpleUrlHandlerMapping。而相应进行实际静态文件查找读取的handler则是ResourceHttpRequestHandler。 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;
}

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

被折叠的 条评论
为什么被折叠?



