목차
0. 환경
- Spring Boot 2.5.6 (Gradle)
- JDK 11(Java 11)
- IntelliJ
- Postman
[build.gradle dependencies]
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation group: 'commons-io', name: 'commons-io', version: '2.6'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.0'
implementation group: 'commons-codec', name: 'commons-codec', version: '1.5'
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.4' //DatatypeConverter
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
1. Request Body 핸들링 목적
- API 서버에 통신 시 보안을 위해 클라이언트에서 Request Data(Request Body)를 암호화하여 보낸다는 가정
- 스프링 구조에서 가장 먼저 Request를 받아서 처리하는 부분인 필터를 활용하여 암호화된 Request Body를 복호화 처리합니다.
- Tomcat은 서블릿 기반으로 요청을 처리하므로 Http Request는 HttpServletRequest(javax.servlet)로 넘어오게 됩니다.
- HttpServletRequest를 래핑 하여 커스텀 가능한 HttpServletRequestWrapper를 상속받아서 Request Body를 획득하여 복호화하는 방식으로 구현하면 됩니다.
2. 컨트롤러 생성
- 복호화 된 데이터를 받을 컨트롤러를 생성해줍니다.
- 호출 성공 시 success를 응답합니다.
import com.test.api.service.TestService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@Log4j2
@RestController
public class TestController {
@GetMapping(value = "/test")
public String test(@RequestBody(required = true) String requestBody) throws Exception {
log.info("request Data: " + requestBody);
return "success";
}
}
3. 필터 생성
- @WebFilter 어노테이션을 활용해 Spring Boot 프로젝트에 필터를 추가합니다.
1. 필터 소스 위에 @WebFilter 어노테이션을 이용해 필터링할 패턴을 추가해줍니다.
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*
init()
웹 컨테이너(톰캣)이 시작될 때 필터 최초 한 번 인스턴스 생성
doFilter()
클라이언트의 요청 시 전/후 처리
FilterChain을 통해 전달
public void destroy()
필터 인스턴스가 제거될 때 실행되는 메서드, 종료하는 기능
*/
@Log4j2
@WebFilter(urlPatterns = "/*")
public class apiFilter implements Filter {
/*
- 필터 인스턴스 초기화
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("---필터 인스턴스 초기화---");
}
/*
- 전/후 처리
- Request, Response가 필터를 거칠 때 수행되는 메소드
- chain.doFilter() 기점으로 request, response 나눠집니다.
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String requestURI = req.getRequestURI();
log.info("---Request(" + requestURI + ") 필터---");
chain.doFilter(req, res);
log.info("---Response(" + requestURI + ") 필터---");
}
/*
- 필터 인스턴스 종료
*/
@Override
public void destroy() {
log.info("---필터 인스턴스 종료---");
}
}
2. @SpringBootApplication 어노테이션을 가지고 있는 스프링 부트 실행 파일에 @ServletComponentScan 어노테이션을 추가하면 필터 세팅이 완료됩니다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan //추가
@SpringBootApplication
public class ApiApplication {
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
}
4. HttpServletRequestWrapper
- Http Request는 HttpServletRequest(javax.servlet)로 넘어오게 되는데 HttpServletRequest를 래핑 하여 커스텀 가능한 HttpServletRequestWrapper를 상속받아서 Request Body를 획득하여 복호화하는 방식으로 구현합니다.
- 클래스명은 RequestBodyDecodingWrapper로 작성합니다. (편한 클래스명을 사용해도 됩니다.)
- 편한 예시를 위해 Hex값을 받아 필터에서 Decode 하는 방식으로 구현했습니다. (복호화)
import lombok.extern.log4j.Log4j2;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
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;
@Log4j2
public class RequestBodyDecryptWrapper extends HttpServletRequestWrapper {
// 가로챈 데이터를 가공하여 담을 final 변수
private final String requestDecryptBody;
public RequestBodyDecryptWrapper(HttpServletRequest request) throws IOException, DecoderException {
super(request);
String requestHashData = requestDataByte(request); // Request Data 가로채기
String decodeTemp = requestBodyDecode(requestHashData); // Reqeust Data Hex 디코드
log.info("인코딩 데이터: " + requestHashData);
log.info("디코딩 데이터: " + decodeTemp);
requestDecryptBody = decodeTemp;
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestDecryptBody.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()));
}
//==request Body 가로채기==//
private String requestDataByte(HttpServletRequest request) throws IOException {
byte[] rawData = new byte[128];
InputStream inputStream = request.getInputStream();
rawData = IOUtils.toByteArray(inputStream);
return new String(rawData);
}
//==request Body Hex 디코딩==//
private String requestBodyDecode(String requestHashData) throws DecoderException {
return HexDecodeToString(requestHashData);
}
//==Request Data Decode (Hex Decode)==//
public static String HexDecodeToString(String encodeText) throws DecoderException {
return new String(Hex.decodeHex(encodeText.toCharArray()));
}
}
5. 필터에 적용
- HttpServletRequestWrapper를 상속받아 구현한 RequestBodyDecryptWrapper를 필터에 적용합니다.
- Filter의 doFilter()에 적용합니다.
- 필터에서 일어난 예외는 컨트롤러까지 닿지 않으므로 다음과 같은 형태로 직접 예외처리해줘야 합니다.
@Log4j2
@WebFilter(urlPatterns = "/*")
public class apiFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
try {
// 커스텀 래퍼적용
RequestBodyDecryptWrapper requestWrapper = new RequestBodyDecryptWrapper(req);
chain.doFilter(requestWrapper, res);
} catch (Exception e) {
// 디코딩 불가 예외 처리
log.error(e);
log.error("디코딩 불가능 합니다.");
String ResponseMessage = "디코드 불가능한 데이터 입니다.";
byte[] data = ResponseMessage.getBytes("utf-8");
int count = data.length;
res.setStatus(400);
res.setContentLength(count);
res.getOutputStream().write(data);
res.flushBuffer();
}
}
}
6. 테스트
- 테스트를 위해 작성한 스프링 부트를 실행 후 Postman을 이용해 테스트해 봅니다.
[Test Hex Data]
ec9594ed98b8ed9994eb909c20eb8db0ec9db4ed84b020ec9e85eb8b88eb8ba42e
= 암호화된 데이터입니다.
6.1 정상 호출
- Request Body에 정상 Hex Hash 데이터를 입력하여 호출합니다.
- Response Body에 success를 반환받은 것을 확인할 수 있습니다.
- 인텔리제이 로그에 정상적으로 데이터가 들어와서 인코딩 데이터, 디코딩 데이터, 그리고 컨트롤러로 정상적으로 들어간 것을 확인 할 수 있습니다.
6.2 비 정상 호출
- Request Body에 비정상 Test Hex Data를 입력하여 호출합니다.
- Test Hex Data 뒤에 1을 붙여서 비정상적인 데이터를 만들어 줍니다.
'Backend > Spring' 카테고리의 다른 글
[Spring Boot] 스프링 부트 Log4J2 추가 취약점 조치 (CVE-2021-45046) (0) | 2021.12.15 |
---|---|
[Spring Boot] 스프링 부트 Log4J2 취약점 조치 (Log4J2 버전 업데이트) (3) | 2021.12.13 |
[Spring Boot] REST API 예외처리(Response Json) (0) | 2021.12.01 |
[Spring Boot] 윈도우 스프링 부트 Gradle 프로젝트 jar 빌드 방법 (0) | 2021.11.30 |
[Spring Boot] 스프링 부트 필터 적용 (Filter) (0) | 2021.11.29 |