关于zuul中对于POST请求中path参数和form body中重复参数被过滤的情况
在使用zuul做转发功能时,发现当一个Content-Type为application/x-www-form-urlencoded;charset=UTF-8的POST请求中,path参数和body中的参数重复时,转发的时候会丢失body中的参数,下面是定位的过程
从请求进入ZuulServlet的service方法开始
1 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { 2 try { 3 this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse); 4 RequestContext context = RequestContext.getCurrentContext(); 5 context.setZuulEngineRan(); 6 7 try { 8 this.preRoute(); 9 } catch (ZuulException var13) { 10 this.error(var13); 11 this.postRoute(); 12 return; 13 } 14 15 try { 16 this.route(); 17 } catch (ZuulException var12) { 18 this.error(var12); 19 this.postRoute(); 20 return; 21 } 22 23 try { 24 this.postRoute(); 25 } catch (ZuulException var11) { 26 this.error(var11); 27 } 28 } catch (Throwable var14) { 29 this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName())); 30 } finally { 31 RequestContext.getCurrentContext().unset(); 32 } 33 }
开始做了初始化,然后调用了preRoute方法,这个主要是通过FilterProcessor拿到类型为pre的filter
具体可以看FilterProcessor中的preRoute和runFilter方法
public void preRoute() throws ZuulException { try { this.runFilters("pre"); } catch (ZuulException var2) { throw var2; } catch (Throwable var3) { throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + var3.getClass().getName()); } } public Object runFilters(String sType) throws Throwable { if (RequestContext.getCurrentContext().debugRouting()) { Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); } boolean bResult = false; List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType); if (list != null) { for(int i = 0; i < list.size(); ++i) { ZuulFilter zuulFilter = (ZuulFilter)list.get(i); Object result = this.processZuulFilter(zuulFilter); if (result != null && result instanceof Boolean) { bResult |= (Boolean)result; } } } return bResult; }
通过代码可以看到,拿到type为pre的filter后,遍历执行,zuul中的filter主要有下面这些
其中pre filter有5个
ServletDetectionFilter
Servlet30WrapperFilter
FormBodyWrapperFilter
DebugFilter
PreDecorationFilter
对于请求包装的主要有Servlet30WrapperFilter和FormBodyWrapperFilter
首先我们看一下的代码Servlet30WrapperFilter
public class Servlet30WrapperFilter extends ZuulFilter { private Field requestField = null; public Servlet30WrapperFilter() { this.requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class, "req", HttpServletRequest.class); Assert.notNull(this.requestField, "HttpServletRequestWrapper.req field not found"); this.requestField.setAccessible(true); } protected Field getRequestField() { return this.requestField; } @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return SERVLET_30_WRAPPER_FILTER_ORDER; } @Override public boolean shouldFilter() { return true; // TODO: only if in servlet 3.0 env } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); if (request instanceof HttpServletRequestWrapper) { request = (HttpServletRequest) ReflectionUtils.getField(this.requestField, request); ctx.setRequest(new Servlet30RequestWrapper(request)); } else if (RequestUtils.isDispatcherServletRequest()) { // If it's going through the dispatcher we need to buffer the body ctx.setRequest(new Servlet30RequestWrapper(request)); } return null; } }
发现里面只是把HttpServletRequest封装到了Servlet30RequestWrapper,Servlet30RequestWrapper中没有特别的对参数的处理,排除这个filter,接下来我们看FormBodyWrapperFilter的 run方法的代码
@Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); FormBodyRequestWrapper wrapper = null; if (request instanceof HttpServletRequestWrapper) { HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils .getField(this.requestField, request); wrapper = new FormBodyRequestWrapper(wrapped); ReflectionUtils.setField(this.requestField, request, wrapper); if (request instanceof ServletRequestWrapper) { ReflectionUtils.setField(this.servletRequestField, request, wrapper); } } else { wrapper = new FormBodyRequestWrapper(request); ctx.setRequest(wrapper); } if (wrapper != null) { ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType()); } return null; }
可以看到前面也是把HttpServletRequest(从Servlet30RequestWrapper转成HttpServletRequest)封装成FormBodyRequestWrapper,注意最下面这里
if (wrapper != null) { ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType()); }
调用了wrapper.getContentType(),我们跟踪这个方法发现就是这里对参数做了处理
@Override public String getContentType() { if (this.contentData == null) { buildContentData(); } return this.contentType.toString(); } @Override public int getContentLength() { if (super.getContentLength() <= 0) { return super.getContentLength(); } if (this.contentData == null) { buildContentData(); } return this.contentLength; } public long getContentLengthLong() { return getContentLength(); } @Override public ServletInputStream getInputStream() throws IOException { if (this.contentData == null) { buildContentData(); } return new ServletInputStreamWrapper(this.contentData); } private synchronized void buildContentData() { if (this.contentData != null) { return; } try { MultiValueMap<String, Object> builder = RequestContentDataExtractor .extract(this.request); FormHttpOutputMessage data = new FormHttpOutputMessage(); this.contentType = MediaType.valueOf(this.request.getContentType()); data.getHeaders().setContentType(this.contentType); FormBodyWrapperFilter.this.formHttpMessageConverter.write(builder, this.contentType, data); // copy new content type including multipart boundary this.contentType = data.getHeaders().getContentType(); byte[] input = data.getInput(); this.contentLength = input.length; this.contentData = input; } catch (Exception e) { throw new IllegalStateException("Cannot convert form data", e); } }
在buildContentData方法中调用了RequestContentDataExtractor的extract方法
public final class RequestContentDataExtractor { private RequestContentDataExtractor() { throw new AssertionError("Must not instantiate utility class."); } public static MultiValueMap<String, Object> extract(HttpServletRequest request) throws IOException { return (request instanceof MultipartHttpServletRequest) ? extractFromMultipartRequest((MultipartHttpServletRequest) request) : extractFromRequest(request); } private static MultiValueMap<String, Object> extractFromRequest( HttpServletRequest request) throws IOException { MultiValueMap<String, Object> builder = new LinkedMultiValueMap<>(); Set<String> queryParams = findQueryParams(request); for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) { String key = entry.getKey(); if (!queryParams.contains(key) && entry.getValue() != null) { for (String value : entry.getValue()) { builder.add(key, value); } } } return builder; } private static MultiValueMap<String, Object> extractFromMultipartRequest( MultipartHttpServletRequest request) throws IOException { MultiValueMap<String, Object> builder = new LinkedMultiValueMap<>(); Map<String, List<String>> queryParamsGroupedByName = findQueryParamsGroupedByName( request); Set<String> queryParams = findQueryParams(request); for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) { String key = entry.getKey(); List<String> listOfAllParams = stream(request.getParameterMap().get(key)) .collect(Collectors.toList()); List<String> listOfOnlyQueryParams = queryParamsGroupedByName.get(key); if (listOfOnlyQueryParams != null) { listOfOnlyQueryParams = listOfOnlyQueryParams.stream() .filter(Objects::nonNull) .map(param -> uriDecode(param, Charset.defaultCharset())) .collect(Collectors.toList()); if (!listOfOnlyQueryParams.containsAll(listOfAllParams)) { listOfAllParams.removeAll(listOfOnlyQueryParams); for (String value : listOfAllParams) { builder.add(key, new HttpEntity<>(value, newHttpHeaders(request, key))); } } } if (!queryParams.contains(key)) { for (String value : entry.getValue()) { builder.add(key, new HttpEntity<>(value, newHttpHeaders(request, key))); } } } for (Entry<String, List<MultipartFile>> parts : request.getMultiFileMap() .entrySet()) { for (MultipartFile file : parts.getValue()) { HttpHeaders headers = new HttpHeaders(); headers.setContentDispositionFormData(file.getName(), file.getOriginalFilename()); if (file.getContentType() != null) { headers.setContentType(MediaType.valueOf(file.getContentType())); } HttpEntity entity = new HttpEntity<>( new InputStreamResource(file.getInputStream()), headers); builder.add(parts.getKey(), entity); } } return builder; } private static HttpHeaders newHttpHeaders(MultipartHttpServletRequest request, String key) { HttpHeaders headers = new HttpHeaders(); String type = request.getMultipartContentType(key); if (type != null) { headers.setContentType(MediaType.valueOf(type)); } return headers; } private static Set<String> findQueryParams(HttpServletRequest request) { Set<String> result = new HashSet<>(); String query = request.getQueryString(); if (query != null) { for (String value : tokenizeToStringArray(query, "&")) { if (value.contains("=")) { value = value.substring(0, value.indexOf("=")); } result.add(value); } } return result; } static Map<String, List<String>> findQueryParamsGroupedByName( HttpServletRequest request) { String query = request.getQueryString(); if (isEmpty(query)) { return emptyMap(); } return UriComponentsBuilder.fromUriString("?" + query).build().getQueryParams(); } }
从extract的逻辑看到,会调用extractFromRequest方法,在这个方法中先拿到了queryParams,也就是path的参数,然后通过equest.getParameterMap(),拿到了form参数
这里面有个判断逻辑:if (!queryParams.contains(key) && entry.getValue() != null),当参数在queryParams有的时候不会进行处理,所以到这就真相大白了。至于如何修改,待续......