2020/07/15 - [Programming/Java] - [Spring Boot] Filter (1) - Request Body Modify
이전 글에서 말했듯이 특정 API를 통해 송수신되는 모든 데이터를 암호화해야 하는 니즈로 인해, 들어오는 요청 데이터를 복호화하는 필터 클래스를 작성하였고, 이번에는 요청을 처리한 결과의 응답 데이터를 암호화하는 필터 클래스를 작성해보겠다.
먼저 @RestController 어노테이션이 선언된 Controller 클래스의 경우 @RestController 어노테이션 안에 있는 @ReponseBody 어노테이션에 의해 응답 데이터가 view로 반환되지 않고, MessageConverter 클래스로 인해 JSON 데이터로 변환되고 HTTP Response Body에 직접 작성된다.
Http Response Body에 직접 데이터가 써지기 때문에, ServletOutputStream을 통해서 해당 데이터를 가져와야 한다.
필터 단계에서 API 응답 데이터를 가져오기 위한 ServletOutputStream을 상속받은 FilterServletOutputStream 클래스를 작성해봤다. 생성자 부분에서 OutPutStream 추상 클래스를 인자로 받아, ByteStream을 지원하는 DataOutputStream 클래스 객체를 생성하고, write 메서드 안에 새로 생성된 DataOutPutStream 객체의 write 메서드를 호출하여 데이터를 읽어 들이도록 하였다.
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class FilterServletOutputStream extends ServletOutputStream {
private final DataOutputStream outputStream;
public FilterServletOutputStream(OutputStream output) {
this.outputStream = new DataOutputStream(output);
}
@Override
public void write(int b) throws IOException {
outputStream.write(b);
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener listener) {
}
}
이제 응답 데이터를 FilterServletOutputStream을 통해 가로채고, 이를 가져오는 ResponseBodyEncryptWrapper 클래스를 작성해보겠다.
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ResponseBodyEncryptWrapper extends HttpServletResponseWrapper {
ByteArrayOutputStream output;
FilterServletOutputStream filterOutput;
/**
* Constructs a response adaptor wrapping the given response.
*
* @param response The response to be wrapped
* @throws IllegalArgumentException if the response is null
*/
public ResponseBodyEncryptWrapper(HttpServletResponse response) {
super(response);
output = new ByteArrayOutputStream();
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (filterOutput == null) {
filterOutput = new FilterServletOutputStream(output);
}
return filterOutput;
}
public byte[] getDataStream() {
return output.toByteArray();
}
}
HttpServletResponseWrapper 클래스를 상속받은 ResponseBodyEncodingWrapper 클래스의 코드 양은 그렇게 많지 않다.
가장 먼저 바이트 스트림 데이터를 담기 위한 ByteArrayOutputStream 클래스 객체를 ResponseBodyEncodingWrapper 클래스의 생성자 부분에서 초기화를 해준다. 또한 HttpServletResponseWrapper 클래스의 getOutputStream 메서드를 오버라이딩 하여, ResponseBodyEncodingWrapper 클래스의 생성자 부분에서 초기화 한 ByteArrayOutputStream 객체를 생성자의 인자로 받는 FilterServletOutputStream 객체를 만들고 이를 리턴하도록 작성한다.
이렇게 함으로써 API 응답 데이터를 Http Response Body에 작성할 때, FilterServletOutputStream 객체를 호출한 뒤에 write 메서드를 통해 데이터를 넣게 되고, 해당 데이터는 FilterServletOutputStream 클래스의 생성자의 인자로 넘긴 ByteArrayOutputStream에 실제로 데이터가 쌓이기 때문에, ByteArrayOutputStream 객체의 toByteArray() 메서드를 통해 byte [] 타입으로 실제 API 응답 데이터를 가져올 수 있다.
그럼 이제 ResponseBodyEncodingWrapper 클래스를 활용한 ResponseBodyEncryptFilter 클래스는 다음과 같다.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Slf4j
public class ResponseBodyEncryptFilter implements Filter {
private final String SECRET_KEY = "암호화키";
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ResponseBodyEncryptWrapper responseWrapper = new ResponseBodyEncryptWrapper((HttpServletResponse) response);
chain.doFilter(request, responseWrapper);
String responseMessage = new String(responseWrapper.getDataStream(), StandardCharsets.UTF_8);
log.info("[RESPONSE][ENCRYPT][BEFORE] - [{}]", responseMessage);
String encodedResponse = AES256Utils.encrypt(SECRET_KEY, responseMessage);
String encodingResponseMessage = encodingResponseMessageConverter(encodedResponse);
log.info("[RESPONSE][ENCRYPT][AFTER] - [{}]", encodingResponseMessage);
response.getOutputStream().write(encodingResponseMessage.getBytes());
}
public String encodingResponseMessageConverter(String encodingMessage) throws JsonProcessingException {
EncodingRequestResponse encodingRequestResponse = new EncodingRequestResponse();
encodingRequestResponse.setData(encodingMessage);
return objectMapper.writeValueAsString(encodingRequestResponse);
}
}
ResponseBodyEncryptFilter는 심플하게 구성된다. 먼저 ResponseBodyEncodingWrapper 클래스 객체를 ServletRequest 파라미터를 바탕으로 생성하다. 그 다음 chain.doFilter() 메서드의 파라미터로 넘겨 다른 필터 클래스들이 처리할 수 있도록 ServletRequest와 Wrapper 클래스 객체를 넘기고, Filter Chaining을 통해 처리가 이루어진 다음에 다시 ResponseBodyEncryptFilter로 돌아왔을때 응답 데이터를 문자열로 받고, AES 256 암호화를 진행한 다음에 ServletResponse의 ouputstream을 통해 response body에 암호화된 문자열을 byte 배열로 넣어주면 된다.
이전과 마찬가지로 FilterRegistrationBean을 통해 필터를 등록해주어 Filter를 적용한다.
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer, WebMvcRegistrations {
private static final List<String> SECRET_API_URL = Arrays.asList("/api/secret1","/api/secret2");
@Bean
public FilterRegistrationBean<RequestBodyDecryptFilter> decodingFilter() {
FilterRegistrationBean<ResponseBodyEncryptFilter> responseEncodingFilterBean = new FilterRegistrationBean<>();
responseEncodingFilterBean.setFilter(new ResponseBodyEncryptFilter());
responseEncodingFilterBean.setUrlPatterns(SECRET_API_URLS);
return responseEncodingFilterBean;
}
}
끝!
'Programming > Spring' 카테고리의 다른 글
[SpringBoot] Spring Boot 2.3.8 도커 컨테이너 만들기 (BuildPack) (0) | 2021.01.24 |
---|---|
[Spring Cloud] Spring Cloud Gateway - 다운스트림 로그 확인 (1) | 2020.11.06 |
[Spring Boot] Filter (1) - Request Body Modify (0) | 2020.07.15 |
[SpringBoot] Dockerizing a Spring Boot Application (0) | 2019.11.19 |
[Spring Boot] Java Servlet Filter for Logging Request Parameter (2) | 2019.11.12 |