본문으로 바로가기

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 핸들링 목적

스프링 영역(컨트롤러, 서비스 등)에서 처리된 응답을 필터에서 핸들링하여 암호화를 하기 위한 목적으로 핸들링합니다.

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]

postman response

 

[Spring Boot 콘솔 출력]

console 결과