Permintaan Http Servlet kehilangan parameter dari badan POST setelah membacanya sekali

88

Saya mencoba mengakses dua parameter permintaan http dalam filter Java Servlet, tidak ada yang baru di sini, tetapi terkejut menemukan bahwa parameter tersebut telah dikonsumsi! Karena itu, filter tidak lagi tersedia di rantai filter.

Tampaknya ini hanya terjadi ketika parameter datang dalam badan permintaan POST (misalnya, formulir yang dikirim).

Apakah ada cara untuk membaca parameter dan TIDAK mengkonsumsinya?

Sejauh ini saya hanya menemukan referensi ini: Filter Servlet menggunakan request.getParameter kehilangan data Formulir .

Terima kasih!

amuniz
sumber
2
mungkin menunjukkan potongan kode bagaimana Anda melakukannya?
Pavel Veller
Apakah Anda mendapatkan getInputStream () atau getReader ()? Sepertinya merekalah yang akan mengganggu eksekusi getParameter ()
evanwong

Jawaban:

115

Selain itu, cara alternatif untuk mengatasi masalah ini adalah dengan tidak menggunakan rantai filter dan malah membangun komponen pencegat Anda sendiri, mungkin menggunakan aspek, yang dapat beroperasi pada isi permintaan yang diurai. Ini juga mungkin akan lebih efisien karena Anda hanya mengonversi permintaan InputStreammenjadi objek model Anda sendiri satu kali.

Namun, menurut saya masuk akal untuk ingin membaca isi permintaan lebih dari sekali, terutama saat permintaan berpindah melalui rantai filter. Saya biasanya akan menggunakan rantai filter untuk operasi tertentu yang ingin saya pertahankan di lapisan HTTP, dipisahkan dari komponen layanan.

Seperti yang disarankan oleh Will Hartung, saya mencapai ini dengan memperluas HttpServletRequestWrapper, mengonsumsi permintaan, InputStreamdan pada dasarnya meng-cache byte.

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

Sekarang badan permintaan bisa dibaca lebih dari sekali dengan membungkus permintaan asli sebelum meneruskannya melalui rantai filter:

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

Solusi ini juga akan memungkinkan Anda membaca isi permintaan beberapa kali melalui getParameterXXXmetode karena panggilan dasarnya adalah getInputStream(), yang tentu saja akan membaca permintaan yang di-cache InputStream.

Sunting

Untuk versi ServletInputStreamantarmuka yang lebih baru . Anda perlu memberikan implementasi beberapa metode lagi seperti isReady, setReadListenerdll. Lihat pertanyaan ini seperti yang diberikan dalam komentar di bawah.

pestrella
sumber
5
Benarkah itu? Panggilan yang mendasarinya adalah getInputStream () pada permintaan asli , yang byte-nya sudah Anda baca. Permintaan yang mendasari tidak memiliki pengetahuan tentang pembungkus Anda, jadi bagaimana ia tahu untuk memanggil getInputStream () pembungkus?
Frans
1
Tepatnya getInputStreamdipanggil pada pembungkus saya karena ini adalah ServletRequestcontoh yang saya berikan ke rantai filter. Jika Anda masih ragu, baca kode sumber ServletRequestWrapperdan ServletRequestantarmukanya.
pestrella
1
Jika saya bisa membuat ini +100, saya akan melakukannya. Saya sudah mencoba membuat ini berfungsi dengan benar selama 3-4 jam. Terima kasih atas contoh dan penjelasan Anda yang jelas! Saya senang saya menemukan posting ini!
Doug
21
Ada saran bagaimana membuat ini bekerja dengan Servlet-api 3.0+? ServletInputStream sekarang memiliki abstrak isReady(). isFinished()dan setReadListener()untuk menangani IO non-pemblokiran yang harus diterapkan. Saya pikir ReadListener dapat dikosongkan, tetapi tidak yakin apa yang harus dilakukan isFinished()dan / atau isReady().
Eric B.
12
@Tokopedia terima kasih. Saya kemudian menemukan solusi untuk antarmuka api yang lebih baru, cukup ditempelkan di sini jika ada yang tertarik. stackoverflow.com/questions/29208456/…
dcc
37

Saya tahu saya terlambat, tetapi pertanyaan ini masih relevan untuk saya dan posting SO ini adalah salah satu hit teratas di Google. Saya akan melanjutkan dan memposting solusi saya dengan harapan orang lain dapat menghemat beberapa jam.

Dalam kasus saya, saya perlu mencatat semua permintaan dan tanggapan dengan tubuh mereka. Menggunakan Spring Framework jawabannya sebenarnya cukup sederhana, cukup gunakan ContentCachingRequestWrapper dan 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
sumber
3
Ini tidak berhasil untuk saya. Keduanya requestBodydan responseBodymerupakan string kosong
Abhijith Madhav
5
Kesalahanku. Saya melakukan chain.doFilter(request, response);alih - alihchain.doFilter(requestWrapper, responseWrapper);
Abhijith Madhav
5
The ContentCaching*Wrapperkelas memiliki harga mahal mengkonsumsi input stream sehingga "caching" dilakukan melalui metode getContentAsByteArraytetapi kelas ini tidak caching aliran masukan yang mungkin dibutuhkan oleh filter lainnya dalam rantai filter (yang menggunakan kasus saya). Imho, ini adalah perilaku yang tidak diharapkan dari kelas cache konten, oleh karena itu saya mengangkat peningkatan ini di tim musim semi jira.spring.io/browse/SPR-16028
Federico Piazza
Anda dapat menggunakan AbstractRequestLoggingFilterdari Spring, di mana sebagian besar pekerjaan telah dilakukan oleh Spring dan Anda hanya perlu mengganti 1 atau 2 metode sederhana.
keras
1
Ini tidak berhasil untuk saya sejak spring-web-4.3.12.RELEASE. Ketika saya memeriksa sumber, saya menemukan variabel cachedContentdigunakan untuk menyimpan berbagai konten seperti parameter permintaan dan permintaan inputStream. Kosong jika Anda getContentAsByteArray()hanya menelepon . Untuk mendapatkan badan permintaan, Anda harus menelepon getInputStream(). Tapi sekali lagi, ini akan membuat inputStream tidak tersedia untuk filter dan penangan lain.
Ivan Huang
7

Jawaban di atas sangat membantu, tetapi masih ada beberapa masalah dalam pengalaman saya. Pada tomcat 7 servlet 3.0, getParamter dan getParamterValues ​​juga harus ditimpa. Solusi di sini mencakup parameter get-query dan post-body. Ini memungkinkan untuk mendapatkan string mentah dengan mudah.

Seperti solusi lainnya, ia menggunakan Apache commons-io dan Googles Guava.

Dalam solusi ini, metode getParameter * tidak menampilkan IOException tetapi menggunakan super.getInputStream () (untuk mendapatkan isi) yang mungkin menampilkan IOException. Saya menangkapnya dan membuang runtimeException. Ini tidak terlalu bagus.

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
sumber
Ini bagus! Saya sudah mencoba memikirkan hal ini selama berhari-hari, dan ini bekerja dengan servlet 3.1. Satu pertanyaan: mengapa kau lakukan decode(getPostBodyAsString(), result);di getParameterMap()? Itu membuat parameter dengan key = request body dan value = null, yang cukup aneh.
orlade
Daripada melalui semua parsing string, mengapa Anda tidak memanggil super.getParameterMap()Anda getParameterMap? Yang akan memberi Anda peta <String, String[]>.
Ean V
6

Satu-satunya cara adalah Anda menggunakan sendiri seluruh aliran input di filter, mengambil apa yang Anda inginkan darinya, lalu membuat InputStream baru untuk konten yang Anda baca, dan memasukkan InputStream itu ke ServletRequestWrapper (atau HttpServletRequestWrapper).

Sisi negatifnya adalah Anda harus mengurai sendiri muatannya, standar tidak membuat kemampuan itu tersedia untuk Anda.

Tambahan -

Seperti yang saya katakan, Anda perlu melihat HttpServletRequestWrapper.

Dalam sebuah filter, Anda melanjutkan dengan memanggil FilterChain.doFilter (request, response).

Untuk filter sepele, permintaan dan respons sama dengan yang diteruskan ke filter. Tidak harus demikian. Anda dapat menggantinya dengan permintaan dan / atau tanggapan Anda sendiri.

HttpServletRequestWrapper dirancang khusus untuk memfasilitasi ini. Anda meneruskannya pada permintaan awal, dan kemudian Anda dapat menghentikan semua panggilan. Anda membuat subkelas Anda sendiri untuk ini, dan mengganti metode getInputStream dengan salah satu milik Anda. Anda tidak dapat mengubah aliran masukan dari permintaan asli, jadi Anda memiliki pembungkus ini dan mengembalikan aliran masukan Anda sendiri.

Kasus paling sederhana adalah menggunakan aliran input permintaan asli ke buffer byte, melakukan sihir apa pun yang Anda inginkan, lalu membuat ByteArrayInputStream baru dari buffer itu. Inilah yang dikembalikan di pembungkus Anda, yang diteruskan ke metode FilterChain.doFilter.

Anda harus membuat subkelas ServletInputStream dan membuat pembungkus lain untuk ByteArrayInputStream Anda, tetapi itu juga bukan masalah besar.

Will Hartung
sumber
Saya tidak dapat membaca InputStream dan memulihkannya setelahnya, tidak ada metode get / set untuk akses langsung ke aliran. Proposal Anda tampaknya bagus, tetapi saya tidak mengerti bagaimana cara mengimplementasikannya.
amuniz
5

Saya juga mengalami masalah yang sama dan saya yakin kode di bawah ini lebih sederhana dan berfungsi untuk saya,

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

di kelas filter java,

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

Beri tahu saya jika Anda memiliki pertanyaan

Panjang dan kurus
sumber
3

Jadi ini pada dasarnya adalah jawaban Lathy TAPI diperbarui untuk persyaratan yang lebih baru untuk ServletInputStream.

Yaitu (untuk ServletInputStream), seseorang harus mengimplementasikan:

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

Ini adalah objek Lathy yang sudah diedit

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

dan di suatu tempat (??) saya menemukan ini (yang merupakan kelas kelas satu yang berhubungan dengan metode "ekstra".

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

Pada akhirnya, saya hanya mencoba mencatat permintaan. Dan potongan-potongan frankensteined di atas membantu saya membuat di bawah ini.

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

Silakan ambil kode ini dengan sebutir garam.

"Tes" PALING penting adalah jika POST bekerja dengan payload. Inilah yang akan mengungkap masalah "baca ganda".

kode contoh palsu

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

Anda dapat mengganti "MyCustomObject" dengan "Object" biasa jika Anda hanya ingin menguji.

Jawaban ini jujur ​​dari beberapa posting dan contoh SOF yang berbeda .. tapi butuh beberapa saat untuk menyatukan semuanya jadi saya harap ini membantu pembaca di masa mendatang.

Tolong beri suara positif pada jawaban Lathy sebelum jawaban saya. Aku tidak akan bisa sejauh ini tanpanya.

Di bawah ini adalah salah satu / beberapa pengecualian yang saya dapatkan saat mengerjakan ini.

getReader () telah dipanggil untuk permintaan ini

Sepertinya beberapa tempat yang "saya pinjam" ada di sini:

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

Spring memiliki dukungan bawaan untuk ini dengan 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;
}

Sayangnya Anda masih tidak dapat membaca payload langsung dari permintaan, tetapi parameter pesan String akan menyertakan payload sehingga Anda dapat mengambilnya dari sana sebagai berikut:

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

Markoorn
sumber
Saya berharap dapat menggunakan solusi Anda untuk menghasilkan log audit tetapi saya perlu mencatat apakah permintaan tersebut berhasil, dapatkah saya menghubungkan ke respons http dan mendapatkan kode di dalam kelas ini.
jonesy
1

Hanya menimpa getInputStream()tidak berhasil dalam kasus saya. Implementasi server saya tampaknya mengurai parameter tanpa memanggil metode ini. Saya tidak menemukan cara lain, tetapi menerapkan ulang keempat metode getParameter * juga. Berikut adalah kode getParameterMap(Apache Http Client dan pustaka Google Guava digunakan):

@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;
}
30 jam
sumber
1
Sayangnya, Jetty mengalami masalah ini, grepcode.com/file/repo1.maven.org/maven2/org.eclipse.jetty/…
mikeapr4
Anda mungkin menggunakan Tomcat 7 atau lebih tinggi dengan Servlet 3.0? Apakah Anda juga memiliki kode untuk 3 metode lainnya?
Perak
3 metode lainnya cukup panggil getParameterMap () dan ambil nilai yang diperlukan.
30
0

Jika Anda memiliki kendali atas permintaan tersebut, Anda dapat mengatur tipe konten ke aliran biner / oktet . Hal ini memungkinkan untuk meminta parameter tanpa menghabiskan aliran input.

Namun, ini mungkin khusus untuk beberapa server aplikasi. Saya hanya menguji tomcat, jetty tampaknya berperilaku dengan cara yang sama menurut https://stackoverflow.com/a/11434646/957103 .

Olivier.Roger
sumber
0

Metode getContentAsByteArray () dari kelas Spring ContentCachingRequestWrapper membaca isi beberapa kali, tetapi metode getInputStream () dan getReader () dari kelas yang sama tidak membaca isi beberapa kali:

"Kelas ini meng-cache badan permintaan dengan menggunakan InputStream. Jika kita membaca InputStream di salah satu filter, filter berikutnya di rantai filter tidak dapat membacanya lagi. Karena batasan ini, kelas ini tidak cocok di semua situasi. "

Dalam kasus saya, solusi yang lebih umum yang memecahkan masalah ini adalah menambahkan tiga kelas berikut ke proyek boot Spring saya (dan dependensi yang diperlukan ke file 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);
    }
}

Saya juga menambahkan dependensi berikut ke 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>

Sebuah tuturial dan kode sumber lengkap ada di sini: https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

Veli-Matti Sorvala
sumber
0

Saya menemukan solusi yang bagus untuk semua format badan permintaan. Saya menguji application/x-www-form-urlencodeddan application/jsonkeduanya bekerja dengan sangat baik. Masalah ContentCachingRequestWrapperitu dirancang hanya untuk x-www-form-urlencodedbadan permintaan, tetapi tidak bekerja dengan misalnya json. Saya menemukan solusi untuk tautan json . Ada masalah yang tidak didukungnya x-www-form-urlencoded. Saya bergabung dengan keduanya di kode saya:

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

    }

}
Andrew Sneck
sumber
-1

Anda dapat menggunakan rantai filter servlet, tetapi sebaliknya menggunakan yang asli, Anda dapat membuat permintaan Anda sendiri yourownrequests extends HttpServletRequestWrapper.

Zhengwei Zhan
sumber
1
Sepertinya link ke tutorial mengandung virus sekarang.
puasa
-2

Pertama-tama kita tidak harus membaca parameter di dalam filter. Biasanya header dibaca di filter untuk melakukan beberapa tugas otentikasi. Karena itu seseorang dapat membaca isi HttpRequest sepenuhnya di Filter atau Interceptor dengan menggunakan CharStreams:

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

Ini sama sekali tidak mempengaruhi pembacaan berikutnya.

ashoka
sumber
ya itu benar. Jika Anda melakukan ini sekali, request.getReader()akan mengembalikan pembaca yang hanya berisi string kosong pada bacaan berikutnya.
christophetd
1
Saya akan bekerja jika menimpa metode getReader () dan getInputStream () untuk menggunakan badan baru ini sebagai sumber.
Rodrigo Borba