form data 는 쿼리파라미터로 들어가는데 post 하면 url에 노출되어 안좋지 않을까?
multipart/form-data도 일종의 form-data 이기 때문에 쿼리파라미터 일 것이다. 그러므로 HttpRequestServlet
의 요청 파라미터 값들을 조회하면 나와야한다. 그런데 왜 문자열 key value들은 잘 나오는데, 이미지 파일에 대한 파라미터 정보는 나오지 않을까?
getParts()
를 이용하여 각 파트를 조회하면 알 수 있다.그런데 왜 컨트롤러에서 Http body로 들어온 내용을 HttpRequestServlet
을 이용하여 정보를 출력하려고 하면 비어있는 상태일까?
HttpRequestServlet
의 body의 정보를 출력했을 경우에는 body 값이 그대로 잘 찍히는 모습을 확인할 수 있다.HttpServletRequest
의 getInputStream()
을 Servlet의 Filter에서 사용하면 다른 필터나 Controller에서 해당 요청에 대한 getInputStream
을 가져갈 수 없다.
@RestController
public class TestController {
@PostMapping("/")
public void bodyEmpty(@RequestBody User user, HttpServletRequest request) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
System.out.println("Http Body = " + messageBody);
System.out.println("Http Body Length = " + request.getContentLength());
}
}
위와 같이 컨트롤러에서 HttpServletRequest
를 이용하여 Http Body 값을 출력하려고 한다. Postman을 이용하여 {"name" : "name", "password":"password"}
를 body에 담아 post 요청을 보내게 되면 다음과 같은 결과가 콘솔에 출력 된다.
Http Body =
Http Body Length = 38
Body의 길이는 잘 출력되지만 Body에 원래 들어있는 내용은 출력되지 않는 모습을 확인할 수 있다.
아래와 같이 Http Body가 소비되는 시점을 대략적으로 파악했었다.
요청 -> 디스패처 서블릿 -> 컨트롤러의 객체로 값을 바인딩 하는 과정에서 바디 데이터 소비 -> 컨트롤러의 request body 비워져 있음
이를 기반으로 찾아본 결과 오라클 문서에서 다음과 같은 내용을 찾아볼 수 있었다.
If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream() or getReader() can interfere with the execution of this method.
이를 토대로 Interceptor
에서 getInputStream()
을 호출하여 Http Body를 읽으면서 소비하였기 때문에, Controller의 Http Body가 비어있음을 알 수 있었다.
그렇다면 Http Body를 어떻게하면 Controller에서 확인할 수 있을까? 먼저 Http 요청이 들어오는 차례를 알아보자. Client에서 요청이 들어오면 해당 요청은 Controller에 들어오기 전 Filter, Dispatcher Servlet, Interceptor를 거쳐 Controller에 들어오게 된다.
위에서 발견한 내용이라면 Http Body는 컨트롤러에 들어오기 전 Interceptor에서 소비되고 있었다.
그렇다면 Interceptor보다 먼저 호출되는 Filter 단계에서 Http Body를 저장해놓는다면 Controller에서 출력할 수 있을 것 같아 해당 방법을 찾아보게 되었다.
방법은 다음과 같다.
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(cachedBytes.toByteArray());
}
@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 input stream which reads the cached request body */
private static class CachedServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public CachedServletInputStream(byte[] contents) {
this.buffer = new ByteArrayInputStream(contents);
}
@Override
public int read() {
return buffer.read();
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
}
}
한번 읽고 사라지는 HttpBody가 아닌 여러번 계속해서 읽을 수 있도록 구현하기 위해서, 먼저 HttpServletRequestWrapper
를 상속받는 MultiReadHttpServletRequest
클래스를 하나 만든다. 그리고 여기서 getInputStream()
에서 요청 Http Body를 복사하여 값을 저장하고 반환하는 기능을 재정의 한다.
그리고 이를 다음과 같이 MyFilter
라는 새로운 필터 클래스를 생성한 뒤 다음과 같이 MultiReadHttpServletRequest
클래스 생성을 필터에서 실행해준다.
@Component
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
chain.doFilter(multiReadRequest, response);
}
}
이렇게 되면 인터셉터에서 Http Body가 소모 되기 전에 필터 단계에서 Http Body를 옮겨 담는 기능이 수행되기 때문에, Controller 에서 Http Body 값을 확인할 수 있게 된다.
아래는 필터를 적용한 뒤 콘솔창의 출력 결과이다.
Http Body = {"name":"name", "password":"password"}
Http Body Length = 38