Http Servlet request lose params from POST body after read it once

前端 未结 13 2109
一个人的身影
一个人的身影 2020-11-22 14:56

I\'m trying to accessing two http request parameters in a Java Servlet filter, nothing new here, but was surprised to find that the parameters have already been consumed! Be

相关标签:
13条回答
  • 2020-11-22 15:47

    If you have control over the request, you could set the content type to binary/octet-stream. This allows to query for parameters without consuming the input stream.

    However, this might be specific to some application servers. I only tested tomcat, jetty seems to behave the same way according to https://stackoverflow.com/a/11434646/957103.

    0 讨论(0)
  • 2020-11-22 15:48

    The above answers were very helpful, but still had some problems in my experience. On tomcat 7 servlet 3.0, the getParamter and getParamterValues also had to be overwritten. The solution here includes both get-query parameters and the post-body. It allows for getting raw-string easily.

    Like the other solutions it uses Apache commons-io and Googles Guava.

    In this solution the getParameter* methods do not throw IOException but they use super.getInputStream() (to get the body) which may throw IOException. I catch it and throw runtimeException. It is not so nice.

    import com.google.common.collect.Iterables;
    import com.google.common.collect.ObjectArrays;
    
    import org.apache.commons.io.IOUtils;
    import org.apache.http.NameValuePair;
    import org.apache.http.client.utils.URLEncodedUtils;
    import org.apache.http.entity.ContentType;
    
    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.UnsupportedEncodingException;
    import java.nio.charset.Charset;
    import java.util.Collections;
    import java.util.LinkedHashMap;
    import java.util.List;
    import java.util.Map;
    
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    
    /**
     * Purpose of this class is to make getParameter() return post data AND also be able to get entire
     * body-string. In native implementation any of those two works, but not both together.
     */
    public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
        public static final String UTF8 = "UTF-8";
        public static final Charset UTF8_CHARSET = Charset.forName(UTF8);
        private ByteArrayOutputStream cachedBytes;
        private Map<String, String[]> parameterMap;
    
        public MultiReadHttpServletRequest(HttpServletRequest request) {
            super(request);
        }
    
        public static void toMap(Iterable<NameValuePair> inputParams, Map<String, String[]> toMap) {
            for (NameValuePair e : inputParams) {
                String key = e.getName();
                String value = e.getValue();
                if (toMap.containsKey(key)) {
                    String[] newValue = ObjectArrays.concat(toMap.get(key), value);
                    toMap.remove(key);
                    toMap.put(key, newValue);
                } else {
                    toMap.put(key, new String[]{value});
                }
            }
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            if (cachedBytes == null) cacheInputStream();
            return new CachedServletInputStream();
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }
    
        private void cacheInputStream() throws IOException {
        /* Cache the inputStream in order to read it multiple times. For
         * convenience, I use apache.commons IOUtils
         */
            cachedBytes = new ByteArrayOutputStream();
            IOUtils.copy(super.getInputStream(), cachedBytes);
        }
    
        @Override
        public String getParameter(String key) {
            Map<String, String[]> parameterMap = getParameterMap();
            String[] values = parameterMap.get(key);
            return values != null && values.length > 0 ? values[0] : null;
        }
    
        @Override
        public String[] getParameterValues(String key) {
            Map<String, String[]> parameterMap = getParameterMap();
            return parameterMap.get(key);
        }
    
        @Override
        public Map<String, String[]> getParameterMap() {
            if (parameterMap == null) {
                Map<String, String[]> result = new LinkedHashMap<String, String[]>();
                decode(getQueryString(), result);
                decode(getPostBodyAsString(), result);
                parameterMap = Collections.unmodifiableMap(result);
            }
            return parameterMap;
        }
    
        private void decode(String queryString, Map<String, String[]> result) {
            if (queryString != null) toMap(decodeParams(queryString), result);
        }
    
        private Iterable<NameValuePair> decodeParams(String body) {
            Iterable<NameValuePair> params = URLEncodedUtils.parse(body, UTF8_CHARSET);
            try {
                String cts = getContentType();
                if (cts != null) {
                    ContentType ct = ContentType.parse(cts);
                    if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
                        List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), UTF8_CHARSET);
                        params = Iterables.concat(params, postParams);
                    }
                }
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
            return params;
        }
    
        public String getPostBodyAsString() {
            try {
                if (cachedBytes == null) cacheInputStream();
                return cachedBytes.toString(UTF8);
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        /* An inputStream which reads the cached request body */
        public class CachedServletInputStream extends ServletInputStream {
            private ByteArrayInputStream input;
    
            public CachedServletInputStream() {
                /* create a new input stream from the cached request body */
                input = new ByteArrayInputStream(cachedBytes.toByteArray());
            }
    
            @Override
            public int read() throws IOException {
                return input.read();
            }
        }
    
        @Override
        public String toString() {
            String query = dk.bnr.util.StringUtil.nullToEmpty(getQueryString());
            StringBuilder sb = new StringBuilder();
            sb.append("URL='").append(getRequestURI()).append(query.isEmpty() ? "" : "?" + query).append("', body='");
            sb.append(getPostBodyAsString());
            sb.append("'");
            return sb.toString();
        }
    }
    
    0 讨论(0)
  • 2020-11-22 15:50

    The method getContentAsByteArray() of the Spring class ContentCachingRequestWrapper reads the body multiple times, but the methods getInputStream() and getReader() of the same class do not read the body multiple times:

    "This class caches the request body by consuming the InputStream. If we read the InputStream in one of the filters, then other subsequent filters in the filter chain can't read it anymore. Because of this limitation, this class is not suitable in all situations."

    In my case more general solution that solved this problem was to add following three classes to my Spring boot project (and the required dependencies to the pom file):

    CachedBodyHttpServletRequest.java:

    public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
    
        private byte[] cachedBody;
    
        public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
            super(request);
            InputStream requestInputStream = request.getInputStream();
            this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            return new CachedBodyServletInputStream(this.cachedBody);
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            // Create a reader from cachedContent
            // and return it
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
            return new BufferedReader(new InputStreamReader(byteArrayInputStream));
        }
    }
    

    CachedBodyServletInputStream.java:

    public class CachedBodyServletInputStream extends ServletInputStream {
    
        private InputStream cachedBodyInputStream;
    
        public CachedBodyServletInputStream(byte[] cachedBody) {
            this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
        }
    
        @Override
        public boolean isFinished() {
            try {
                return cachedBodyInputStream.available() == 0;
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return false;
        }
    
        @Override
        public boolean isReady() {
            return true;
        }
    
        @Override
        public void setReadListener(ReadListener readListener) {
            throw new UnsupportedOperationException();
        }
    
        @Override
        public int read() throws IOException {
            return cachedBodyInputStream.read();
        }
    }
    

    ContentCachingFilter.java:

    @Order(value = Ordered.HIGHEST_PRECEDENCE)
    @Component
    @WebFilter(filterName = "ContentCachingFilter", urlPatterns = "/*")
    public class ContentCachingFilter extends OncePerRequestFilter {
    
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            System.out.println("IN  ContentCachingFilter ");
            CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(httpServletRequest);
            filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse);
        }
    }
    

    I also added the following dependencies to pom:

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.10.0</version>
    </dependency>
    

    A tuturial and full source code is located here: https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

    0 讨论(0)
  • 2020-11-22 15:52

    I too had the same issue and I believe the code below is more simple and it is working for me,

    public class MultiReadHttpServletRequest extends  HttpServletRequestWrapper {
    
     private String _body;
    
    public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException {
       super(request);
       _body = "";
       BufferedReader bufferedReader = request.getReader();           
       String line;
       while ((line = bufferedReader.readLine()) != null){
           _body += line;
       }
    }
    
    @Override
    public ServletInputStream getInputStream() throws IOException {
       final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(_body.getBytes());
       return new ServletInputStream() {
           public int read() throws IOException {
               return byteArrayInputStream.read();
           }
       };
    }
    
    @Override
    public BufferedReader getReader() throws IOException {
       return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
    }
    

    in the filter java class,

    HttpServletRequest properRequest = ((HttpServletRequest) req);
    MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(properRequest);
    req = wrappedRequest;
    inputJson = IOUtils.toString(req.getReader());
    System.out.println("body"+inputJson);
    

    Please let me know if you have any queries

    0 讨论(0)
  • 2020-11-22 15:53

    As an aside, an alternative way to solve this problem is to not use the filter chain and instead build your own interceptor component, perhaps using aspects, which can operate on the parsed request body. It will also likely be more efficient as you are only converting the request InputStream into your own model object once.

    However, I still think it's reasonable to want to read the request body more than once particularly as the request moves through the filter chain. I would typically use filter chains for certain operations that I want to keep at the HTTP layer, decoupled from the service components.

    As suggested by Will Hartung I achieved this by extending HttpServletRequestWrapper, consuming the request InputStream and essentially caching the bytes.

    public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
      private ByteArrayOutputStream cachedBytes;
    
      public MultiReadHttpServletRequest(HttpServletRequest request) {
        super(request);
      }
    
      @Override
      public ServletInputStream getInputStream() throws IOException {
        if (cachedBytes == null)
          cacheInputStream();
    
          return new CachedServletInputStream();
      }
    
      @Override
      public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(getInputStream()));
      }
    
      private void cacheInputStream() throws IOException {
        /* Cache the inputstream in order to read it multiple times. For
         * convenience, I use apache.commons IOUtils
         */
        cachedBytes = new ByteArrayOutputStream();
        IOUtils.copy(super.getInputStream(), cachedBytes);
      }
    
      /* An inputstream which reads the cached request body */
      public class CachedServletInputStream extends ServletInputStream {
        private ByteArrayInputStream input;
    
        public CachedServletInputStream() {
          /* create a new input stream from the cached request body */
          input = new ByteArrayInputStream(cachedBytes.toByteArray());
        }
    
        @Override
        public int read() throws IOException {
          return input.read();
        }
      }
    }
    

    Now the request body can be read more than once by wrapping the original request before passing it through the filter chain:

    public class MyFilter implements Filter {
      @Override
      public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
    
        /* wrap the request in order to read the inputstream multiple times */
        MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
    
        /* here I read the inputstream and do my thing with it; when I pass the
         * wrapped request through the filter chain, the rest of the filters, and
         * request handlers may read the cached inputstream
         */
        doMyThing(multiReadRequest.getInputStream());
        //OR
        anotherUsage(multiReadRequest.getReader());
        chain.doFilter(multiReadRequest, response);
      }
    }
    

    This solution will also allow you to read the request body multiple times via the getParameterXXX methods because the underlying call is getInputStream(), which will of course read the cached request InputStream.

    Edit

    For newer version of ServletInputStream interface. You need to provide implementation of few more methods like isReady, setReadListener etc. Refer this question as provided in comment below.

    0 讨论(0)
  • 2020-11-22 15:54

    Just overwriting of getInputStream() did not work in my case. My server implementation seems to parse parameters without calling this method. I did not find any other way, but re-implement the all four getParameter* methods as well. Here is the code of getParameterMap (Apache Http Client and Google Guava library used):

    @Override
    public Map<String, String[]> getParameterMap() {
        Iterable<NameValuePair> params = URLEncodedUtils.parse(getQueryString(), NullUtils.UTF8);
    
        try {
            String cts = getContentType();
            if (cts != null) {
                ContentType ct = ContentType.parse(cts);
                if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
                    List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), NullUtils.UTF8);
                    params = Iterables.concat(params, postParams);
                }
            }
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        Map<String, String[]> result = toMap(params);
        return result;
    }
    
    public static Map<String, String[]> toMap(Iterable<NameValuePair> body) {
        Map<String, String[]> result = new LinkedHashMap<>();
        for (NameValuePair e : body) {
            String key = e.getName();
            String value = e.getValue();
            if (result.containsKey(key)) {
                String[] newValue = ObjectArrays.concat(result.get(key), value);
                result.remove(key);
                result.put(key, newValue);
            } else {
                result.put(key, new String[] {value});
            }
        }
        return result;
    }
    
    0 讨论(0)
提交回复
热议问题