HTTP 서블릿 요청을 한 번 읽은 후 POST 본문에서 매개 변수가 손실됩니다.


88

Java Servlet 필터에서 두 개의 http 요청 매개 변수에 액세스하려고합니다. 여기에는 새로운 것이 없지만 매개 변수가 이미 사용되었다는 사실에 놀랐습니다! 이로 인해 필터 체인에서 더 이상 사용할 수 없습니다.

이는 매개 변수가 POST 요청 본문 (예 : 양식 제출)에 들어올 때만 발생하는 것 같습니다.

매개 변수를 읽고 소비하지 않는 방법이 있습니까?

지금까지이 참조 만 찾았습니다. Servlet Filter using request.getParameter loses Form data .

감사!


2
어떻게하는지 코드 조각을 보여줄 수 있습니까?
Pavel Veller 2012

getInputStream () 또는 getReader ()를 얻었습니까? 보인다 그들은의 getParameter ()의 실행을 방해하는 사람처럼
evanwong

답변:


115

제쳐두고,이 문제를 해결하는 다른 방법은 필터 체인을 사용하지 않고 대신 파싱 된 요청 본문에서 작동 할 수있는 측면을 사용하여 자체 인터셉터 구성 요소를 빌드하는 것입니다. 요청 InputStream을 자신의 모델 객체로 한 번만 변환하기 때문에 더 효율적일 것 입니다.

그러나 특히 요청이 필터 체인을 통해 이동할 때 요청 본문을 두 번 이상 읽는 것이 합리적이라고 생각합니다. 일반적으로 서비스 구성 요소와 분리 된 HTTP 계층에서 유지하려는 특정 작업에 필터 체인을 사용합니다.

Will Hartung이 제안한 대로을 확장 HttpServletRequestWrapper하고 요청을 소비 InputStream하며 본질적으로 바이트를 캐싱하여 이를 달성했습니다 .

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();
    }
  }
}

이제 요청 본문은 필터 체인을 통과하기 전에 원래 요청을 래핑하여 두 번 이상 읽을 수 있습니다.

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);
  }
}

이 솔루션을 사용하면 getParameterXXX기본 호출이 getInputStream()물론 캐시 된 요청을 읽을 수 있기 때문에 메서드 를 통해 요청 본문을 여러 번 읽을 수 있습니다 InputStream.

편집하다

최신 버전의 ServletInputStream인터페이스. isReady, setReadListener등과 같은 몇 가지 메소드의 구현을 제공해야 합니다. 아래 설명에 제공된대로이 질문 을 참조하십시오 .


5
사실인가요? 기본 호출은 원래 요청에 대한 getInputStream () 이며 이미 바이트를 읽었을 것입니다. 기본 요청에는 래퍼에 대한 지식이 없으므로 래퍼의 getInputStream () 호출을 어떻게 알 수 있습니까?
Frans 2013 년

1
이것이 내가 필터 체인에 전달 하는 인스턴스 이기 때문에 래퍼 getInputStream에서 정확하게 호출됩니다 . 여전히 의심 스러우면의 소스 코드 와 인터페이스 를 읽으십시오 . ServletRequestServletRequestWrapperServletRequest
pestrella

1
이 +100을 만들 수 있다면 그렇게 할 것입니다. 나는 이것을 3-4 시간 동안 제대로 작동 시키려고 노력 해왔다. 명확한 예와 설명에 감사드립니다! 이 게시물을 찾아서 다행입니다!
Doug

21
Servlet-api 3.0+에서이 작업을 수행하는 방법에 대한 제안이 있습니까? ServletInputStream에는 이제 abstract가 isReady()있습니다. isFinished()setReadListener()구현해야합니다 비 차단 IO를 처리합니다. ReadListener가 비어있을 수 있다고 생각하지만 isFinished()및 / 또는 isReady().
Eric B.

12
@EricB. 어쨌든 감사합니다. 나중에 누군가가 관심이있는 경우 여기에 붙여 넣은 최신 API 인터페이스에 대한 솔루션을 찾았습니다. stackoverflow.com/questions/29208456/…
dcc

37

나는 내가 늦었다는 것을 알고 있지만이 질문은 여전히 ​​나에게 관련이 있었고이 SO 게시물은 Google에서 가장 인기있는 게시물 중 하나였습니다. 다른 사람이 몇 시간을 절약 할 수 있기를 바라며 내 솔루션을 게시하겠습니다.

제 경우에는 모든 요청과 응답을 시체와 함께 기록해야했습니다. Spring Framework를 사용하면 대답은 실제로 매우 간단 합니다. ContentCachingRequestWrapperContentCachingResponseWrapper를 사용하면 됩니다.

import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class LoggingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

        try {
            chain.doFilter(requestWrapper, responseWrapper);
        } finally {

            String requestBody = new String(requestWrapper.getContentAsByteArray());
            String responseBody = new String(responseWrapper.getContentAsByteArray());
            // Do not forget this line after reading response content or actual response will be empty!
            responseWrapper.copyBodyToResponse();

            // Write request and response body, headers, timestamps etc. to log files

        }

    }

}

3
이것은 나를 위해 작동하지 않았습니다. 둘 다 requestBodyresponseBody있었다 빈 문자열
앱 히지 스 Madhav

5
내 실수. 나는하고 있던 chain.doFilter(request, response);대신 중chain.doFilter(requestWrapper, responseWrapper);
앱 히지 스 Madhav

5
ContentCaching*Wrapper클래스는 "캐시"이 방법을 통해 수행되도록 입력 스트림 소비의 비싼 가격이 getContentAsByteArray있지만,이 클래스는 (내 사용 사례 인) 필터 체인에 다른 필터가 필요로 될 수있는 입력 스트림을 캐싱되지 않습니다. 이럴이 그러므로 내가 봄 팀이 개선 제기, 콘텐츠 캐싱 클래스의하지 예상되는 동작입니다 jira.spring.io/browse/SPR-16028
페데리코 광장

AbstractRequestLoggingFilter대부분의 작업이 이미 Spring에 의해 수행되고 1 개 또는 2 개의 간단한 메서드 만 재정의하면되는 Spring에서 사용할 수 있습니다 .
가혹한

1
이것은 나를 위해 작동하지 않습니다 spring-web-4.3.12.RELEASE. 소스를 확인한 결과 변수 cachedContent가 요청 매개 변수 및 요청 inputStream과 같은 다양한 내용을 저장하는 데 사용 된다는 것을 알았습니다 . getContentAsByteArray()전적으로 전화하면 비어 있습니다 . 요청 본문을 얻으려면을 호출해야합니다 getInputStream(). 그러나 다시 말하지만 이것은 inputStream을 다른 필터와 핸들러에서 사용할 수 없게 만듭니다.
이반 황

7

위의 답변은 매우 유용했지만 내 경험에는 여전히 문제가있었습니다. tomcat 7 서블릿 3.0에서는 getParamter 및 getParamterValues도 덮어 써야했습니다. 이 솔루션에는 get-query 매개 변수와 post-body가 모두 포함됩니다. 원시 문자열을 쉽게 얻을 수 있습니다.

다른 솔루션과 마찬가지로 Apache commons-io 및 Googles Guava를 사용합니다.

이 솔루션에서 getParameter * 메서드는 IOException을 throw하지 않지만 IOException을 throw 할 수있는 super.getInputStream () (본문을 가져 오기 위해)을 사용합니다. 나는 그것을 잡아서 runtimeException을 던진다. 그렇게 좋지 않습니다.

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();
    }
}

이것은 대단합니다! 나는 이것을 며칠 동안 알아 내려고 노력해 왔으며 이것은 서블릿 3.1에서 작동합니다. 한 가지 질문 : 왜 decode(getPostBodyAsString(), result);에서 getParameterMap()합니까? 키 = 요청 본문 및 값 = null 인 매개 변수를 생성합니다. 이는 매우 이상합니다.
orlade

오히려 문자열 구문 분석을 모두 통과하는 것보다, 왜 전화하지 않는 super.getParameterMap()당신에 getParameterMap? <String, String[]>어쨌든 당신에게지도를 줄 것입니다 .
Ean V

6

유일한 방법은 필터에서 전체 입력 스트림을 직접 소비하고 원하는 것을 가져온 다음 읽은 콘텐츠에 대한 새 InputStream을 만들고 해당 InputStream을 ServletRequestWrapper (또는 HttpServletRequestWrapper)에 넣는 것입니다.

단점은 페이로드를 직접 구문 분석해야한다는 것입니다. 표준에서는 해당 기능을 사용할 수 없습니다.

부록-

앞서 말했듯이 HttpServletRequestWrapper를 살펴 봐야합니다.

필터에서 FilterChain.doFilter (request, response)를 호출하여 계속 진행합니다.

사소한 필터의 경우 요청 및 응답은 필터에 전달 된 것과 동일합니다. 그럴 필요는 없습니다. 이를 자신의 요청 및 / 또는 응답으로 바꿀 수 있습니다.

HttpServletRequestWrapper는이를 용이하게하도록 특별히 설계되었습니다. 원래 요청을 전달한 다음 모든 호출을 가로 챌 수 있습니다. 이것의 고유 한 하위 클래스를 만들고 getInputStream 메서드를 고유 한 것으로 대체합니다. 원래 요청의 입력 스트림을 변경할 수 없으므로 대신이 래퍼가 있고 자체 입력 스트림을 반환합니다.

가장 간단한 경우는 원래 요청 입력 스트림을 바이트 버퍼로 소비하고 원하는 마법을 수행 한 다음 해당 버퍼에서 새 ByteArrayInputStream을 만드는 것입니다. 이것은 FilterChain.doFilter 메소드로 전달되는 랩퍼에서 리턴되는 것입니다.

ServletInputStream의 하위 클래스를 만들고 ByteArrayInputStream에 대한 또 다른 래퍼를 만들어야하지만 그다지 큰 문제는 아닙니다.


InputStream을 읽고 복원 할 수 없으며 스트림에 직접 액세스 할 수있는 get / set 메서드가 없습니다. 귀하의 제안은 좋은 것 같지만 구현 방법을 모르겠습니다.
amuniz

5

나도 같은 문제가 있었고 아래 코드가 더 간단하고 저에게 효과적이라고 생각합니다.

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()));
}
}

필터 자바 클래스에서,

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

궁금한 점이 있으면 알려주세요.


3

따라서 이것은 기본적으로 Lathy의 답변이지만 ServletInputStream에 대한 새로운 요구 사항에 대해 업데이트되었습니다.

즉 (ServletInputStream의 경우) 다음을 구현해야합니다.

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

이것은 편집 된 Lathy의 개체입니다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class RequestWrapper extends HttpServletRequestWrapper {

    private String _body;

    public RequestWrapper(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 {

        CustomServletInputStream kid = new CustomServletInputStream(_body.getBytes());
        return kid;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

그리고 어딘가에서 (??) 이것을 발견했습니다 ( "추가"메소드를 다루는 일류 클래스입니다.

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class CustomServletInputStream extends ServletInputStream {

    private byte[] myBytes;

    private int lastIndexRetrieved = -1;
    private ReadListener readListener = null;

    public CustomServletInputStream(String s) {
        try {
            this.myBytes = s.getBytes("UTF-8");
        } catch (UnsupportedEncodingException ex) {
            throw new IllegalStateException("JVM did not support UTF-8", ex);
        }
    }

    public CustomServletInputStream(byte[] inputBytes) {
        this.myBytes = inputBytes;
    }

    @Override
    public boolean isFinished() {
        return (lastIndexRetrieved == myBytes.length - 1);
    }

    @Override
    public boolean isReady() {
        // This implementation will never block
        // We also never need to call the readListener from this method, as this method will never return false
        return isFinished();
    }

    @Override
    public void setReadListener(ReadListener readListener) {
        this.readListener = readListener;
        if (!isFinished()) {
            try {
                readListener.onDataAvailable();
            } catch (IOException e) {
                readListener.onError(e);
            }
        } else {
            try {
                readListener.onAllDataRead();
            } catch (IOException e) {
                readListener.onError(e);
            }
        }
    }

    @Override
    public int read() throws IOException {
        int i;
        if (!isFinished()) {
            i = myBytes[lastIndexRetrieved + 1];
            lastIndexRetrieved++;
            if (isFinished() && (readListener != null)) {
                try {
                    readListener.onAllDataRead();
                } catch (IOException ex) {
                    readListener.onError(ex);
                    throw ex;
                }
            }
            return i;
        } else {
            return -1;
        }
    }
};

궁극적으로 요청을 기록하려고했습니다. 그리고 위의 프랑켄슈타인 조각들은 제가 아래를 만드는 데 도움이되었습니다.

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;

//one or the other based on spring version
//import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;

import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.filter.OncePerRequestFilter;


/**
 * A filter which logs web requests that lead to an error in the system.
 */
@Component
public class LogRequestFilter extends OncePerRequestFilter implements Ordered {

    // I tried apache.commons and slf4g loggers.  (one or the other in these next 2 lines of declaration */
    //private final static org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory.getLog(LogRequestFilter.class);
    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(LogRequestFilter.class);

    // put filter at the end of all other filters to make sure we are processing after all others
    private int order = Ordered.LOWEST_PRECEDENCE - 8;
    private ErrorAttributes errorAttributes;

    @Override
    public int getOrder() {
        return order;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String temp = ""; /* for a breakpoint, remove for production/real code */

        /* change to true for easy way to comment out this code, remove this if-check for production/real code */
        if (false) {
            filterChain.doFilter(request, response);
            return;
        }

        /* make a "copy" to avoid issues with body-can-only-read-once issues */
        RequestWrapper reqWrapper = new RequestWrapper(request);

        int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
        // pass through filter chain to do the actual request handling
        filterChain.doFilter(reqWrapper, response);
        status = response.getStatus();

        try {
            Map<String, Object> traceMap = getTrace(reqWrapper, status);
            // body can only be read after the actual request handling was done!
            this.getBodyFromTheRequestCopy(reqWrapper, traceMap);

            /* now do something with all the pieces of information gatherered */
            this.logTrace(reqWrapper, traceMap);
        } catch (Exception ex) {
            logger.error("LogRequestFilter FAILED: " + ex.getMessage(), ex);
        }
    }

    private void getBodyFromTheRequestCopy(RequestWrapper rw, Map<String, Object> trace) {
        try {
            if (rw != null) {
                byte[] buf = IOUtils.toByteArray(rw.getInputStream());
                //byte[] buf = rw.getInputStream();
                if (buf.length > 0) {
                    String payloadSlimmed;
                    try {
                        String payload = new String(buf, 0, buf.length, rw.getCharacterEncoding());
                        payloadSlimmed = payload.trim().replaceAll(" +", " ");
                    } catch (UnsupportedEncodingException ex) {
                        payloadSlimmed = "[unknown]";
                    }

                    trace.put("body", payloadSlimmed);
                }
            }
        } catch (IOException ioex) {
            trace.put("body", "EXCEPTION: " + ioex.getMessage());
        }
    }

    private void logTrace(HttpServletRequest request, Map<String, Object> trace) {
        Object method = trace.get("method");
        Object path = trace.get("path");
        Object statusCode = trace.get("statusCode");

        logger.info(String.format("%s %s produced an status code '%s'. Trace: '%s'", method, path, statusCode,
                trace));
    }

    protected Map<String, Object> getTrace(HttpServletRequest request, int status) {
        Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");

        Principal principal = request.getUserPrincipal();

        Map<String, Object> trace = new LinkedHashMap<String, Object>();
        trace.put("method", request.getMethod());
        trace.put("path", request.getRequestURI());
        if (null != principal) {
            trace.put("principal", principal.getName());
        }
        trace.put("query", request.getQueryString());
        trace.put("statusCode", status);

        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            trace.put("header:" + key, value);
        }

        if (exception != null && this.errorAttributes != null) {
            trace.put("error", this.errorAttributes
                    .getErrorAttributes((WebRequest) new ServletRequestAttributes(request), true));
        }

        return trace;
    }
}

이 코드를 소금과 함께 가져 가십시오.

가장 중요한 "테스트"는 POST가 페이로드와 함께 작동하는지 여부입니다. 이것이 "이중 읽기"문제를 노출하는 것입니다.

의사 예제 코드

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("myroute")
public class MyController {
    @RequestMapping(method = RequestMethod.POST, produces = "application/json")
    @ResponseBody
    public String getSomethingExample(@RequestBody MyCustomObject input) {

        String returnValue = "";

        return returnValue;
    }
}

테스트하려는 경우 "MyCustomObject"를 일반 ole "Object"로 바꿀 수 있습니다.

이 답변은 여러 다른 SOF 게시물과 예제에서 frankensteined되었지만 모두 함께 가져 오는 데 시간이 걸렸으므로 미래의 독자에게 도움이되기를 바랍니다.

내 전에 Lathy의 답변을 찬성하십시오. 나는 그것 없이는 여기까지 도달 할 수 없었다.

아래는 이것을 해결하는 동안 내가 얻은 예외 중 하나 / 일부입니다.

이 요청에 대해 getReader ()가 이미 호출되었습니다.

내가 "빌린"장소 중 일부는 여기에있는 것 같습니다.

http://slackspace.de/articles/log-request-body-with-spring-boot/

https://github.com/c0nscience/spring-web-logging/blob/master/src/main/java/org/zalando/springframework/web/logging/LoggingFilter.java

https://howtodoinjava.com/servlets/httpservletrequestwrapper-example-read-request-body/

https://www.oodlestechnologies.com/blogs/How-to-create-duplicate-object-of-httpServletRequest-object

https://github.com/c0nscience/spring-web-logging/blob/master/src/main/java/org/zalando/springframework/web/logging/LoggingFilter.java


3

Spring에는 다음과 같은 기능이 내장되어 있습니다 AbstractRequestLoggingFilter.

@Bean
public Filter loggingFilter(){
    final AbstractRequestLoggingFilter filter = new AbstractRequestLoggingFilter() {
        @Override
        protected void beforeRequest(final HttpServletRequest request, final String message) {

        }

        @Override
        protected void afterRequest(final HttpServletRequest request, final String message) {

        }
    };

    filter.setIncludePayload(true);
    filter.setIncludeQueryString(false);
    filter.setMaxPayloadLength(1000000);

    return filter;
}

불행히도 여전히 요청에서 직접 페이로드를 읽을 수는 없지만 String 메시지 매개 변수에는 페이로드가 포함되어 있으므로 다음과 같이 가져올 수 있습니다.

String body = message.substring(message.indexOf("{"), message.lastIndexOf("]"));


솔루션을 사용하여 감사 로그를 생성하려고했지만 요청이 성공했는지 여부를 기록해야합니다. http 응답에 연결하여이 클래스 내에서 코드를 가져올 수 있습니까?
jonesy

1

getInputStream()제 경우에는 덮어 쓰기만으로 작동하지 않았습니다. 내 서버 구현은이 메서드를 호출하지 않고 매개 변수를 구문 분석하는 것 같습니다. 다른 방법을 찾지 못했지만 4 개의 getParameter * 메서드도 모두 다시 구현합니다. 다음은 getParameterMap(Apache Http Client 및 Google Guava 라이브러리 사용) 의 코드입니다 .

@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;
}


Servlet 3.0과 함께 Tomcat 7 이상을 사용하고 계십니까? 다른 세 가지 방법에 대한 코드도 있습니까?
Silver

다른 세 가지 메서드는 getParameterMap ()을 호출하고 필요한 값을 가져옵니다.
30thh

0

요청을 제어 할 수있는 경우 콘텐츠 유형을 binary / octet-stream으로 설정할 수 있습니다. 이를 통해 입력 스트림을 사용하지 않고도 매개 변수를 쿼리 할 수 ​​있습니다.

그러나 이것은 일부 응용 프로그램 서버에 국한 될 수 있습니다. 나는 바람둥이 만 테스트했는데 부두는 https://stackoverflow.com/a/11434646/957103 에 따라 동일한 방식으로 작동하는 것 같습니다 .


0

Spring 클래스 ContentCachingRequestWrapper의 getContentAsByteArray () 메소드는 본문을 여러 번 읽지 만 동일한 클래스의 getInputStream () 및 getReader () 메소드는 본문을 여러 번 읽지 않습니다.

"이 클래스는 InputStream을 사용하여 요청 본문을 캐시합니다. 필터 중 하나에서 InputStream을 읽으면 필터 체인의 다른 후속 필터가 더 이상 읽을 수 없습니다.이 제한으로 인해이 클래스는 모든 사용자에게 적합하지 않습니다. 상황. "

제 경우에는이 문제를 해결 한보다 일반적인 해결책은 다음 세 가지 클래스를 Spring 부트 프로젝트에 추가하는 것입니다 (그리고 pom 파일에 대한 필수 종속성).

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);
    }
}

또한 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>

자습서 및 전체 소스 코드는 https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times 에 있습니다.


0

모든 형식의 요청 본문에 대한 좋은 해결책을 찾았습니다. 나는 테스트 application/x-www-form-urlencoded했고 application/json둘 다 매우 잘 작동했습니다. 그 문제는 요청 본문에 ContentCachingRequestWrapper대해서만 설계 x-www-form-urlencoded되었지만 json과는 작동하지 않습니다. json 링크에 대한 해결책을 찾았습니다 . 지원하지 않는 문제가있었습니다 x-www-form-urlencoded. 내 코드에서 두 가지를 모두 결합했습니다.

import org.apache.commons.io.IOUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class MyContentCachingRequestWrapper extends ContentCachingRequestWrapper {

    private byte[] body;

    public MyContentCachingRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        super.getParameterMap(); // init cache in ContentCachingRequestWrapper
        body = super.getContentAsByteArray(); // first option for application/x-www-form-urlencoded
        if (body.length == 0) {
            body = IOUtils.toByteArray(super.getInputStream()); // second option for other body formats
        }
    }

    public byte[] getBody() {
        return body;
    }

    @Override
    public ServletInputStream getInputStream() {
        return new RequestCachingInputStream(body);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
    }

    private static class RequestCachingInputStream extends ServletInputStream {

        private final ByteArrayInputStream inputStream;

        public RequestCachingInputStream(byte[] bytes) {
            inputStream = new ByteArrayInputStream(bytes);
        }

        @Override
        public int read() throws IOException {
            return inputStream.read();
        }

        @Override
        public boolean isFinished() {
            return inputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readlistener) {
        }

    }

}

-1

서블릿 필터 체인을 사용할 수 있지만 대신 원본을 사용하여 자신의 요청을 HttpServletRequestWrapper를 확장하여 직접 만들 수 있습니다.


1
이제 튜토리얼 링크에 바이러스가있는 것 같습니다.
fasth

-2

먼저 필터 내에서 매개 변수를 읽어서는 안됩니다. 일반적으로 헤더는 몇 가지 인증 작업을 수행하기 위해 필터에서 읽습니다. CharStreams를 사용하여 Filter 또는 Interceptor에서 HttpRequest 본문을 완전히 읽을 수 있다고 말했지만 :

String body = com.google.common.io.CharStreams.toString(request.getReader());

이것은 후속 읽기에 전혀 영향을주지 않습니다.


네 그렇습니다. 이 작업을 한 번 수행하면 request.getReader()은 후속 읽기에서 빈 문자열 만 포함하는 판독기를 반환합니다.
christophetd

1
이 새 본문을 소스로 사용하기 위해 getReader () 및 getInputStream () 메서드를 덮어 쓰는 경우 작업합니다.
Rodrigo Borba
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.