Die HTTP-Servlet-Anforderung verliert Parameter aus dem POST-Body, nachdem sie einmal gelesen wurde

85

Ich versuche, auf zwei http-Anforderungsparameter in einem Java-Servlet-Filter zuzugreifen, was hier nichts Neues ist, war aber überrascht, dass die Parameter bereits verwendet wurden! Aus diesem Grund ist es in der Filterkette nicht mehr verfügbar.

Es scheint, dass dies nur auftritt, wenn die Parameter in einem POST-Anforderungshauptteil (z. B. einem Formular zum Senden) eingehen.

Gibt es eine Möglichkeit, die Parameter zu lesen und NICHT zu verbrauchen?

Bisher habe ich nur diese Referenz gefunden: Servlet-Filter mit request.getParameter verliert Formulardaten .

Vielen Dank!

amuniz
quelle
2
Vielleicht ein Codefragment zeigen, wie Sie es machen?
Pavel Veller
Haben Sie getInputStream () oder getReader () erhalten? Scheint, als wären sie diejenigen, die die Ausführung von getParameter ()
evanwong

Antworten:

109

Abgesehen davon besteht eine alternative Möglichkeit zur Lösung dieses Problems darin, nicht die Filterkette zu verwenden und stattdessen eine eigene Interceptor-Komponente zu erstellen, möglicherweise unter Verwendung von Aspekten, die auf dem analysierten Anforderungshauptteil ausgeführt werden können. Dies ist wahrscheinlich auch effizienter, da Sie die Anforderung nur einmal InputStreamin Ihr eigenes Modellobjekt konvertieren .

Ich halte es jedoch immer noch für sinnvoll, den Anforderungshauptteil mehr als einmal lesen zu wollen, insbesondere wenn sich die Anforderung durch die Filterkette bewegt. Normalerweise verwende ich Filterketten für bestimmte Vorgänge, die ich auf der HTTP-Ebene behalten möchte, entkoppelt von den Servicekomponenten.

Wie von Will Hartung vorgeschlagen, habe ich dies erreicht, indem ich HttpServletRequestWrapperdie Anforderung erweitert , verbraucht InputStreamund im Wesentlichen die Bytes zwischengespeichert habe.

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

Jetzt kann der Anforderungshauptteil mehr als einmal gelesen werden, indem die ursprüngliche Anforderung umbrochen wird, bevor sie durch die Filterkette geleitet wird:

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

Mit dieser Lösung können Sie den Anforderungshauptteil auch mehrmals über die getParameterXXXMethoden lesen , da der zugrunde liegende Aufruf lautet getInputStream(), der natürlich die zwischengespeicherte Anforderung liest InputStream.

Bearbeiten

Für neuere Version der ServletInputStreamSchnittstelle. Sie müssen wie Implementierung einiger Methoden zur Verfügung zu stellen isReady, setReadListenerusw. Siehe diese Frage , wie unten in Kommentar versehen.

Pestrella
quelle
5
Ist das wahr? Der zugrunde liegende Aufruf lautet getInputStream () für die ursprüngliche Anforderung , von der Sie die Bytes bereits gelesen haben. Die zugrunde liegende Anforderung kennt Ihren Wrapper nicht. Wie sollte sie also getInputStream () des Wrappers aufrufen?
Frans
1
Um genau zu sein, getInputStreamwird mein Wrapper aufgerufen, da dies die ServletRequestInstanz ist, die ich in die Filterkette übergebe. Wenn Sie immer noch Zweifel haben, lesen Sie den Quellcode für ServletRequestWrapperund die ServletRequestSchnittstelle.
Pestrella
1
Wenn ich diese +100 machen könnte, würde ich. Ich habe versucht, dies für 3-4 Stunden zum Laufen zu bringen. Vielen Dank für Ihr klares Beispiel und Ihre Erklärung! Ich bin froh, dass ich diesen Beitrag gefunden habe!
Doug
20
Irgendwelche Vorschläge, wie dies mit Servlet-api 3.0+ funktioniert? Der ServletInputStream hat jetzt eine Zusammenfassung isReady(). isFinished()und setReadListener()sich mit nicht blockierenden E / A zu befassen, die implementiert werden müssen. Ich denke, der ReadListener könnte leer bleiben, bin mir aber nicht sicher, was ich tun soll isFinished()und / oder isReady().
Eric B.
12
@EricB. Danke trotzdem. Ich habe später die Lösung für die neuere API-Oberfläche gefunden, die hier eingefügt wurde, falls jemand interessiert ist. stackoverflow.com/questions/29208456/...
Bohrturm
36

Ich weiß, dass ich zu spät komme, aber diese Frage war für mich immer noch relevant und dieser SO-Beitrag war einer der Top-Hits in Google. Ich gehe voran und poste meine Lösung in der Hoffnung, dass jemand anderes ein paar Stunden sparen könnte.

In meinem Fall musste ich alle Anfragen und Antworten mit ihren Körpern protokollieren. Mit Spring Framework ist die Antwort eigentlich ganz einfach. Verwenden Sie einfach ContentCachingRequestWrapper und ContentCachingResponseWrapper .

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

        }

    }

}
Mikk
quelle
3
Das hat bei mir nicht funktioniert. Beide requestBodyund responseBodywaren leere Saiten
Abhijith Madhav
5
Mein Fehler. Ich machte eine chain.doFilter(request, response);statt einerchain.doFilter(requestWrapper, responseWrapper);
Abhijith Madhav
5
Die ContentCaching*WrapperKlassen haben den teuren Preis, den Eingabestream zu verbrauchen, so dass das "Caching" über die Methode erfolgt, getContentAsByteArrayaber diese Klasse speichert den Eingabestream nicht zwischen, der möglicherweise von anderen Filtern in der Filterkette benötigt wird (was mein Anwendungsfall ist). Imho, dies ist ein nicht erwartetes Verhalten einer Content-Caching-Klasse, daher habe ich diese Verbesserung im Frühjahrsteam jira.spring.io/browse/SPR-16028
Federico Piazza
Sie können AbstractRequestLoggingFiltervon Spring aus verwenden, wo der größte Teil der Arbeit bereits von Spring erledigt wird und Sie nur 1 oder 2 einfache Methoden überschreiben müssen.
hart
1
Das funktioniert bei mir ab sofort nicht mehr spring-web-4.3.12.RELEASE. Als ich die Quelle überprüfte, stellte ich fest, dass die Variable cachedContentzum Speichern verschiedener Inhalte wie Anforderungsparameter und Anforderungseingabestream verwendet wird. Es ist leer, wenn Sie nur anrufen getContentAsByteArray(). Um den Anfragetext zu erhalten, müssen Sie anrufen getInputStream(). Aber auch dies macht den inputStream für andere Filter und den Handler nicht verfügbar.
Ivan Huang
7

Die obigen Antworten waren sehr hilfreich, hatten aber meiner Erfahrung nach immer noch einige Probleme. Unter Tomcat 7 Servlet 3.0 mussten auch getParamter und getParamterValues ​​überschrieben werden. Die Lösung umfasst hier sowohl Get-Query-Parameter als auch den Post-Body. Es ermöglicht das einfache Erhalten von Rohstrings.

Wie die anderen Lösungen verwendet es Apache Commons-Io und Googles Guava.

In dieser Lösung lösen die getParameter * -Methoden keine IOException aus, verwenden jedoch super.getInputStream () (um den Body abzurufen), wodurch möglicherweise eine IOException ausgelöst wird. Ich fange es und wirf runtimeException. Es ist nicht so schön.

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();
    }
}
Arberg
quelle
Das ist toll! Ich habe seit Tagen versucht, dies herauszufinden, und dies funktioniert mit Servlet 3.1. Eine Frage: Warum machst du decode(getPostBodyAsString(), result);in getParameterMap()? Dadurch wird ein Parameter mit key = request body und value = null erstellt, was ziemlich seltsam ist.
Orlade
Warum rufen Sie nicht super.getParameterMap()Ihre an, anstatt die gesamte String-Analyse durchzugehen getParameterMap? Welches gibt Ihnen eine Karte von <String, String[]>sowieso.
Ean V
5

Die einzige Möglichkeit wäre, den gesamten Eingabestream selbst im Filter zu verbrauchen, das zu übernehmen, was Sie möchten, und dann einen neuen InputStream für den gelesenen Inhalt zu erstellen und diesen InputStream in einen ServletRequestWrapper (oder HttpServletRequestWrapper) einzufügen.

Der Nachteil ist, dass Sie die Nutzlast selbst analysieren müssen. Der Standard stellt Ihnen diese Funktion nicht zur Verfügung.

Nachträge -

Wie gesagt, Sie müssen sich HttpServletRequestWrapper ansehen.

In einem Filter fahren Sie fort, indem Sie FilterChain.doFilter aufrufen (Anfrage, Antwort).

Bei trivialen Filtern stimmen Anforderung und Antwort mit denen überein, die an den Filter übergeben wurden. Das muss nicht so sein. Sie können diese durch Ihre eigenen Anfragen und / oder Antworten ersetzen.

HttpServletRequestWrapper wurde speziell entwickelt, um dies zu erleichtern. Sie übergeben ihm die ursprüngliche Anfrage und können dann alle Anrufe abfangen. Sie erstellen eine eigene Unterklasse davon und ersetzen die Methode getInputStream durch eine eigene. Sie können den Eingabestream der ursprünglichen Anforderung nicht ändern. Stattdessen haben Sie diesen Wrapper und geben Ihren eigenen Eingabestream zurück.

Der einfachste Fall besteht darin, den ursprünglichen Eingabestream für Anforderungen in einen Bytepuffer zu kopieren, die gewünschte Magie auszuführen und dann aus diesem Puffer einen neuen ByteArrayInputStream zu erstellen. Dies wird in Ihrem Wrapper zurückgegeben, der an die FilterChain.doFilter-Methode übergeben wird.

Sie müssen ServletInputStream in eine Unterklasse unterteilen und einen weiteren Wrapper für Ihren ByteArrayInputStream erstellen, aber das ist auch keine große Sache.

Will Hartung
quelle
Ich kann den InputStream nicht lesen und danach wiederherstellen. Es gibt keine get / set-Methoden, um direkt auf den Stream zuzugreifen. Ihr Vorschlag scheint gut zu sein, aber ich sehe keine Umsetzung.
Amuniz
4

Ich hatte auch das gleiche Problem und ich glaube, der folgende Code ist einfacher und funktioniert für mich.

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 der Filter-Java-Klasse

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

Bitte lassen Sie mich wissen, wenn Sie Fragen haben

Lathy
quelle
3

Dies ist also im Grunde Lathys Antwort, ABER aktualisiert für neuere Anforderungen für ServletInputStream.

Nämlich (für ServletInputStream) muss Folgendes implementiert werden:

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

Dies ist das bearbeitete Objekt von 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()));
    }
}

und irgendwo (??) habe ich das gefunden (das ist eine erstklassige Klasse, die sich mit den "zusätzlichen" Methoden befasst.

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

Letztendlich habe ich nur versucht, die Anfragen zu protokollieren. Und die oben genannten frankensteined zusammen Stücke halfen mir, das unten zu schaffen.

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

Bitte nehmen Sie diesen Code mit einem Körnchen Salz.

Der wichtigste "Test" ist, ob ein POST mit einer Nutzlast arbeitet. Dies ist es, was "Double Read" -Probleme aufdeckt.

Pseudo-Beispielcode

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

Sie können "MyCustomObject" durch "Object" ersetzen, wenn Sie nur testen möchten.

Diese Antwort basiert auf verschiedenen SOF-Posts und Beispielen. Es hat jedoch eine Weile gedauert, bis alles zusammengekommen ist. Ich hoffe, es hilft einem zukünftigen Leser.

Bitte stimmen Sie Lathys Antwort vor meiner zu. Ohne sie hätte ich nicht so weit kommen können.

Unten ist eine / einige der Ausnahmen, die ich beim Ausarbeiten bekommen habe.

getReader () wurde bereits für diese Anfrage aufgerufen

Es sieht so aus, als wären einige der Orte, von denen ich "geliehen" habe, hier:

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

granadaCoder
quelle
3

Der Frühling hat hierfür eine integrierte Unterstützung mit 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;
}

Leider können Sie die Nutzdaten immer noch nicht direkt aus der Anforderung lesen, aber der Parameter String message enthält die Nutzdaten, sodass Sie sie von dort aus wie folgt abrufen können:

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

Markoorn
quelle
Ich hatte gehofft, mit Ihrer Lösung ein Überwachungsprotokoll zu erstellen, aber ich muss aufzeichnen, ob die Anforderung erfolgreich war. Kann ich mich in die http-Antwort einbinden und den Code innerhalb dieser Klasse abrufen?
Jonesy
1

Nur das Überschreiben von getInputStream()hat in meinem Fall nicht funktioniert. Meine Serverimplementierung scheint Parameter zu analysieren, ohne diese Methode aufzurufen. Ich habe keinen anderen Weg gefunden, aber auch alle vier getParameter * -Methoden neu implementiert. Hier ist der Code von getParameterMap(Apache Http Client und Google Guava Bibliothek verwendet):

@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;
}
30thh
quelle
1
Jetty hat dieses Problem leider, grepcode.com/file/repo1.maven.org/maven2/org.eclipse.jetty/…
mikeapr4
Sie verwenden wahrscheinlich Tomcat 7 oder höher mit Servlet 3.0? Haben Sie den Code auch für die anderen 3 Methoden?
Silber
Andere 3 Methoden rufen einfach getParameterMap () auf und rufen den benötigten Wert ab.
30.
0

Wenn Sie die Kontrolle über die Anforderung haben, können Sie den Inhaltstyp auf binär / Oktett-Stream setzen . Dies ermöglicht das Abfragen von Parametern, ohne den Eingabestream zu verbrauchen.

Dies kann jedoch für einige Anwendungsserver spezifisch sein. Ich habe nur Tomcat getestet. Der Steg scheint sich laut https://stackoverflow.com/a/11434646/957103 genauso zu verhalten .

Olivier.Roger
quelle
0

Die Methode getContentAsByteArray () der Spring-Klasse ContentCachingRequestWrapper liest den Body mehrmals, aber die Methoden getInputStream () und getReader () derselben Klasse lesen den Body nicht mehrmals:

"Diese Klasse speichert den Anforderungshauptteil zwischen, indem sie den InputStream verwendet. Wenn wir den InputStream in einem der Filter lesen, können andere nachfolgende Filter in der Filterkette ihn nicht mehr lesen. Aufgrund dieser Einschränkung ist diese Klasse nicht für alle geeignet Situationen. "

In meinem Fall bestand eine allgemeinere Lösung, die dieses Problem löste, darin, meinem Spring-Boot-Projekt die folgenden drei Klassen hinzuzufügen (und die erforderlichen Abhängigkeiten zur POM-Datei):

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

Ich habe pom auch die folgenden Abhängigkeiten hinzugefügt:

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

Eine Anleitung und einen vollständigen Quellcode finden Sie hier: https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

Veli-Matti Sorvala
quelle
-1

Sie können die Servlet-Filterkette verwenden, aber stattdessen die ursprüngliche verwenden. Sie können Ihre eigene Anfrage erstellen. Ihre eigenen Anfragen erweitern HttpServletRequestWrapper.

Zhengwei Zhan
quelle
1
Es scheint, dass der Link zum Tutorial jetzt einen Virus enthält.
fasth
-2

Zunächst sollten wir keine Parameter innerhalb des Filters lesen. Normalerweise werden die Header im Filter gelesen, um nur wenige Authentifizierungsaufgaben auszuführen. Allerdings kann man den HttpRequest-Body mithilfe der CharStreams vollständig im Filter oder Interceptor lesen:

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

Dies hat keinerlei Auswirkungen auf die nachfolgenden Lesevorgänge.

Ashoka
quelle
ja tut es. Wenn Sie dies einmal tun, request.getReader()wird ein Reader zurückgegeben, der nur bei den nachfolgenden Lesevorgängen eine leere Zeichenfolge enthält.
Christophetd
1
Ich würde im Falle des Überschreibens der Methoden getReader () und getInputStream () arbeiten, um diesen neuen Körper als Quelle zu verwenden.
Rodrigo Borba