解決Zuul無法同時轉發Multipart和JSON請求的問題
系統中有一個採用 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後發現這個問題 ofollow,noindex">別人也遇到過 ,說這個版本的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.591WARN 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 的擴充套件性挺好的。