解决Zuul无法同时转发Multipart和JSON请求的问题

2,076 阅读3分钟
原文链接: zhuanlan.zhihu.com

系统中有一个采用 Netflix Zuul 实现的网关模块,负责统一的鉴权,然后把请求转到对应的后端模块。基本的配置后,只需要实现一个Filter就可以了。

@Slf4j
@Component
public class AccessTokenFilter extends ZuulFilter {

	// Filter 的类型,在路由之前
    @Override
    public String filterType() {
        return "pre";
    }

	// 比系统的优先级要低些
    @Override
    public int filterOrder() {
        return 7;
    }


    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        HttpServletResponse response = requestContext.getResponse();

        
        String token = CookieUtils.getCookieValue("token", request);
        log.info("token={}", token);

        token = URLDecoder.decode(token, "UTF-8");
		// 验证 token
		boolean valid = validateToken(token);

		// 验证不通过则直接响应
		if(!valid){
			 setFalseZuulResponse(requestContext);
		}

        return null;
    }

    /**
     * 不再路由,直接响应.
     */
    private void setFalseZuulResponse(RequestContext requestContext) {
        requestContext.setSendZuulResponse(false); 
        requestContext.setResponseBody("error");
    }
}

一切都OK,可是有一天出现了问题。

环境

Spring Boot 版本:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

Spring Cloud 版本:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>Brixton.SR5</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

问题背景

有一天,新增了一个接口,URL中带有JSON串,发现访问该接口时请求无法到达后端。网关模块抛出了异常 URISyntaxException。

Caused by: java.net.URISyntaxException: Illegal character in query at index 65: http://10.201.169.146:8091/api/.../?param=%7B"a":"","b":"","c":""%7D
	at java.net.URI$Parser.fail(URI.java:2848)
	at java.net.URI$Parser.checkChars(URI.java:3021)
	at java.net.URI$Parser.parseHierarchical(URI.java:3111)
	at java.net.URI$Parser.parse(URI.java:3053)
	at java.net.URI.<init>(URI.java:588)
	at com.sun.jersey.api.uri.UriBuilderImpl.createURI(UriBuilderImpl.java:721)

很慌,然后Goolge后发现这个问题别人也遇到过,说这个版本的Zuul默认使用的是 Ribbon Client,换成 http client 就可以了。

@Bean
public RibbonCommandFactory<?> ribbonCommandFactory(
        final SpringClientFactory clientFactory) {
    return new HttpClientRibbonCommandFactory(clientFactory);
}

的确解决了这个问题,但是又出现了新的问题:之前的 Multipart/form-data POST 请求转发到后端服务器后出现了 java.io.IOException: Incomplete parts

2018-10-09 19:04:22.591  WARN 12137 --- [qtp289592183-19] o.e.jetty.server.handler.ErrorHandler    : EXCEPTION 

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.io.IOException: Incomplete parts
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:845)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:584)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:566)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:226)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1180)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:512)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1112)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
	at org.eclipse.jetty.server.Dispatcher.forward(Dispatcher.java:199)
	at org.eclipse.jetty.server.Dispatcher.error(Dispatcher.java:79)
	at org.eclipse.jetty.server.handler.ErrorHandler.handle(ErrorHandler.java:94)
	at org.springframework.boot.context.embedded.jetty.JettyEmbeddedErrorHandler.handle(JettyEmbeddedErrorHandler.java:55)
	at org.eclipse.jetty.server.Response.sendError(Response.java:558)
	at org.eclipse.jetty.server.Response.sendError(Response.java:497)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:651)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:226)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1180)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:512)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1112)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:134)
	at org.eclipse.jetty.server.Server.handle(Server.java:534)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:320)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:251)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:273)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:95)
	at org.eclipse.jetty.io.SelectChannelEndPoint$2.run(SelectChannelEndPoint.java:93)
	at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.executeProduceConsume(ExecuteProduceConsume.java:303)
	at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.produceConsume(ExecuteProduceConsume.java:148)
	at org.eclipse.jetty.util.thread.strategy.ExecuteProduceConsume.run(ExecuteProduceConsume.java:136)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:671)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:589)
	at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.io.IOException: Incomplete parts
	at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:111)
	at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:85)
	at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:76)
	at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1099)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:932)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
	... 42 common frames omitted
Caused by: java.io.IOException: Incomplete parts
	at org.eclipse.jetty.util.MultiPartInputStreamParser.parse(MultiPartInputStreamParser.java:781)
	at org.eclipse.jetty.util.MultiPartInputStreamParser.getParts(MultiPartInputStreamParser.java:422)
	at org.eclipse.jetty.server.Request.getParts(Request.java:2317)
	at org.eclipse.jetty.server.Request.extractMultipartParameters(Request.java:519)
	at org.eclipse.jetty.server.Request.extractContentParameters(Request.java:441)
	at org.eclipse.jetty.server.Request.getParameters(Request.java:365)
	at org.eclipse.jetty.server.Request.getParameter(Request.java:996)
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:70)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1699)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1699)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:582)
	... 21 common frames omitted

异常抛的位置是 org.eclipse.jetty.util.MultiPartInputStreamParser#parse

最终没能定位到问题的根本原因。但是问题基本比较清晰了:使用默认的 RibbonCommandFactory(即RestClientRibbonCommandFactory) 可以处理 multipart/form 的请求,但是无法处理URL中含JSON的情况,而如果使用 HttpClientRibbonCommandFactory 则可以处理RUL中含JSON的情况,但是无法正确转发 multipart 的请求。

问题出在路由转发的时候,后来想到能不能换一种思路:自己修改路由转发的逻辑根据请求的类型来指定使用不同的 RibbonCommandFactory?

解决方法

禁掉默认的路由过滤器 RibbonRoutingFilter。

zuul.RibbonRoutingFilter.route.disable: true

然后扩展 RibbonRoutingFilter,修改默认的转发逻辑。

@Slf4j
public class MyRibbonRoutingFilter extends RibbonRoutingFilter {

    @Autowired
    private RestClientRibbonCommandFactory restClientRibbonCommandFactory;

    @Autowired
    private HttpClientRibbonCommandFactory httpClientRibbonCommandFactory;

    public MyRibbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory) {
        super(helper, ribbonCommandFactory);
    }

    public MyRibbonRoutingFilter(RibbonCommandFactory<?> ribbonCommandFactory) {
        super(ribbonCommandFactory);
    }


    protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
        log.info("-------MyRibbonRoutingFilter forward--------");
        Map<String, Object> info = this.helper.debug(context.getVerb(), context.getUri(),
                context.getHeaders(), context.getParams(), context.getRequestEntity());

        RibbonCommandFactory rcf = this.restClientRibbonCommandFactory;

        if (!isMultipartForm()) {
            log.info("Not multipart/form request use HttpClientRibbonCommandFactory to handle url with json");
            rcf = httpClientRibbonCommandFactory;
        } else {
            log.info("Multipart/form request use default");
        }
        log.info("RibbonCommandFactory is " + rcf.getClass().getCanonicalName());

        RibbonCommand command = rcf.create(context);
        try {
            ClientHttpResponse response = command.execute();
            this.helper.appendDebug(info, response.getStatusCode().value(),
                    response.getHeaders());
            return response;
        } catch (HystrixRuntimeException ex) {
            return handleException(info, ex);
        }

    }

    private static boolean isMultipartForm() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String contentType = request.getContentType();
        if (contentType == null) {
            return false;
        }

        try {
            MediaType mediaType = MediaType.valueOf(contentType);
            return MediaType.MULTIPART_FORM_DATA.includes(mediaType);
        } catch (InvalidMediaTypeException ex) {
            return false;
        }
    }
}

当然这里的两个 RibbonCommandFactory bean 需要配置。

@Configuration
public class RibbonCommandFactoryConfig {

    @Bean
    public HttpClientRibbonCommandFactory ribbonCommandFactory(final SpringClientFactory clientFactory) {
        return new HttpClientRibbonCommandFactory(clientFactory);
    }

    @Bean
    public RestClientRibbonCommandFactory ribbonCommandFactory2(final SpringClientFactory clientFactory) {
        return new RestClientRibbonCommandFactory(clientFactory);
    }
}

问题解决了,可以看到 Zuul 的扩展性挺好的。

相关阅读

github.com/Netflix/zuu…


原文地址