0. 환경
- Spring Boot 2.5.6 (Gradle)
- JDK 11(Java 11)
- IntelliJ
- Postman
[build.gradle dependencies]
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-quartz'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
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. Response Body 핸들링 목적
스프링 영역(컨트롤러, 서비스 등)에서 처리된 응답을 필터에서 핸들링하여 암호화를 하기 위한 목적으로 핸들링합니다.
2. 컨트롤러 생성
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.bind.annotation.*;
@RestController
public class TestController {
@GetMapping(value = "/test")
public Response test() {
return new Response("200", "안녕하세요");
}
//==Response DTO==//
@Data
@AllArgsConstructor
static class Response {
private String code;
private String msg;
}
}
3. 필터 생성
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("---필터 인스턴스 종료---");
}
}
4. ServletOutputStream
Response Body 데이터를 가지고 오기 위해서는 ServletOutputStream을 이용해서 데이터를 가지고 와야 합니다.
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class ResponseBodyServletOutputStream extends ServletOutputStream {
private final DataOutputStream outputStream;
public ResponseBodyServletOutputStream(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) {
}
}
5. HttpServletRequestWrapper
HttpServletResponseWrapper를 이용해 처리된 ByteArrayOutputStream 형태의 Response Body Data를 String으로 변환하여 필터에서 받을 수 있게 getDataStreamToString() 메소드를 작성합니다.
import org.apache.commons.io.output.ByteArrayOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ResponseBodyEncryptWrapper extends HttpServletResponseWrapper {
ByteArrayOutputStream byteArrayOutputStream;
ResponseBodyServletOutputStream responseBodyServletOutputStream;
public ResponseBodyEncryptWrapper(HttpServletResponse response) {
super(response);
byteArrayOutputStream = new ByteArrayOutputStream();
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (responseBodyServletOutputStream == null) {
responseBodyServletOutputStream = new ResponseBodyServletOutputStream(byteArrayOutputStream);
}
return responseBodyServletOutputStream;
}
// 가로챈 Response Body Get
public String getDataStreamToString() {
return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8);
}
}
6. 필터에 적용
doFilter()에서 데이터를 가로챈 후 response 합니다.
테스트를 위해 암호화 소스가 아닌 간단한 Hex Encode를 활용했습니다.
/*
- 전/후 처리
- 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;
// Wrapper
ResponseBodyEncryptWrapper responseWrapper = new ResponseBodyEncryptWrapper((HttpServletResponse) response);
chain.doFilter(req, responseWrapper);
// Response Body Data 가지고 옴
String responseMessage = responseWrapper.getDataStreamToString();
System.out.println("암호화 전> " + responseMessage);
System.out.println("암호화 후> " + responseEncrypt(responseMessage));
// Response 처리
responseMessage = responseEncrypt(responseMessage); // 암호화 메소드 호출(Hex)
byte[] responseMessageBytes = responseMessage.getBytes("utf-8");
int contentLength = responseMessageBytes.length;
response.setContentLength(contentLength);
response.getOutputStream().write(responseMessageBytes);
response.flushBuffer(); // marks response as committed
}
7. 테스트
테스트를 위해 작성한 스프링 부트를 실행 후 Postman을 이용해 테스트해 봅니다.
[PostMan]
[Spring Boot 콘솔 출력]
'Backend > Spring' 카테고리의 다른 글
[Kotlin Spring Boot] Kotlin Spring Boot + JPA = 코프링 프로젝트 세팅 (0) | 2023.09.30 |
---|---|
[Spring Boot] 스프링 스케줄 작업 적용하기(@EnableScheduling, @Scheduled) (0) | 2022.02.02 |
[Spring Boot] JWT (JSON Web Token) 토큰 기반 인증 (2) | 2022.01.02 |
[Spring Boot] 스프링 부트 Log4J2 취약점 조치 (CVE-2021-44832) (0) | 2021.12.29 |
[Spring Boot] 스프링 부트 Log4J2 취약점 조치 (CVE-2021-45105) (0) | 2021.12.18 |