스프링 기반으로 서버를 개발하다가, 특정 API를 통해 송 수신되는 모든 데이터를 암호화해야 하는 니즈가 생기게 되었다. 다음은 스프링을 조금 공부해봤다고 하는 사람들은 대다수 알만한 스프링 MVC에서의 요청 처리 구조 사진이다.
보는 것과 같이 요청을 가장 먼저 처리하는 부분은 바로 filter 이다. filter layer에서 요청 데이터를 복호화하고, 이를 그대로 뒷단으로 넘겨서 로직을 처리할 수 있도록 구조를 생각해보았다.
스프링 부트의 기본 웹 서버인 Tomcat은 서블릿 기반으로 웹 요청을 처리하기에, 스프링 부트에서 Http 요청은 javax.servlet.http 패키지 하위에 있는 HttpServletRequest 클래스 객체로 넘어오게 되며, javax.servlet.http 패키지에서는 이를 커스텀할 수 있는 HttpServletRequestWrapper 클래스를 제공한다.
https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequestWrapper.html
HttpServletRequestWrapper를 상속받아서 요청 데이터를 복호화하는 코드는 다음과 같다.
RequestBodyDecodingWraper.class
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
@Slf4j
public class RequestBodyDecodingWrapper extends HttpServletRequestWrapper {
private final String decodingBody;
/**
* Constructs a request object wrapping the given request.
*
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public RequestBodyDecodingWrapper(HttpServletRequest request, final String decryptSecretKey) {
super(request);
// Convert InputStream data to byte array and store it to this wrapper instance.
byte[] rawData;
try {
// inputstream을 얻어온다
InputStream inputStream = request.getInputStream();
// inputstream을 읽어 요청 데이터를 가져온다. (byte)
rawData = IOUtils.toByteArray(inputStream);
} catch (IOException e) {
log.error("[DECRYPT] - REQUEST INPUT STREAM READ FAIL.");
log.error(e.getMessage(), e);
}
try {
// json string value 를 object에 맵핑하기 위해 사용
ObjectMapper objectMapper = new ObjectMapper();
// 따로 만든 DTO 클래스
EncodingRequestResponse encodingRequestResponse = objectMapper.readValue(rawData, EncodingRequestResponse.class);
log.debug("[ENCODED_REQUEST] - {}", encodingRequestResponse.toString());
String url = request.getRequestURI();
log.debug("[ENCODED][REQUEST_BODY][{}] - [{}]", url, encodingRequestResponse.getData());
String decode = AES256Utils.decrypt(decryptSecretKey, encodingRequestResponse.getData());
log.debug("[DECRYPT][REQUEST][{}] - [{}]", url, decode);
this.decodingBody = decode;
} catch (Exception e) {
log.error("[DECRYPT] - REQUEST CONVERT OBJECT FAIL.");
log.error(e.getMessage(), e);
}
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decodingBody.getBytes(StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
이제 위 코드를 이용할 필터 클래스를 만들면된다. FIlter layer에 커스텀한 기능을 구현할 수 있도록 javax.servlet 패키지에서는 Filter 인터페이스를 제공하는데 이를 구현한 코드는 다음과 같다.
RequestBodyDecodingFilter.class
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Slf4j
public class RequestBodyDecryptFilter implements Filter {
private final String SECRET_KEY = "암복호화키";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 커스텀 wrapper 클래스로 요청 데이터를 처리해버리기!
RequestBodyDecodingWrapper requestWrapper = new RequestBodyDecodingWrapper((HttpServletRequest) request, SECRET_KEY);
log.debug("[REQUEST][DECODING_FILTER] - BEGIN.");
// 필터 체인에서 다음 필터를 수행하도록 doFilter 메서드를 호출하고 request, response를 넘긴다
chain.doFilter(requestWrapper, response);
log.debug("[REQUEST][DECODING_FILTER] - END.");
}
}
Spring Boot 에서는 직접 구현한 필터 클래스를 사용하기 위해서는 FilterRegistrationBean 클래스를 활용해서 구현한 필터를 등록을 해줘야하 한다.
WebMVCConfiguration.class
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<RequestBodyDecryptFilter> requestDecodingFilterBean = new FilterRegistrationBean<>();
requestDecodingFilterBean.setFilter(new RequestBodyDecryptFilter());
requestDecodingFilterBean.setUrlPatterns(SECRET_API_URLS);
return requestDecodingFilterBean;
}
}
끝!
'Programming > Spring' 카테고리의 다른 글
[Spring Cloud] Spring Cloud Gateway - 다운스트림 로그 확인 (1) | 2020.11.06 |
---|---|
[Spring Boot] Filter (2) -Response Body Modify (5) | 2020.08.04 |
[SpringBoot] Dockerizing a Spring Boot Application (0) | 2019.11.19 |
[Spring Boot] Java Servlet Filter for Logging Request Parameter (2) | 2019.11.12 |
[Spring Boot] Spring Security with REST API Login AS JSON (8) | 2019.10.23 |