반응형

들어가며

Spring Request Life Cycle

  • Request : User > Filter > DispatcherServlet > Interceptor > Controller
  • Response : Controller > Interceptor > DispatcherServlet > Filter > User

Filter와 Interceptor의 공통점

  • 요청에 대한 전, 후처리가 가능

Filter와 Interceptor의 차이

  • Filter
    • 스프링 컨텍스트 외부에서 실행되므로 빈 사용이 불가능
    • 주로 요청 파라미터 자체의 검증 및 처리를 담당
      • encoding 처리
      • xss 방어 처리 (네이버 lucy-xss-filter 많이 사용하는듯?)
    • 예외 발생시 web.xml에서 정의한 페이지로 처리해야함
  • Interceptor
    • 스프링 컨텍스트 내부에서 실행되므로 빈 사용이 가능함
    • 주로 서비스 로직을 활용하여 요청 정보 정합성 처리를 담당
      • 로그인 체크
      • 권한 체크
    • 예외 발생시 ControllerAdvice, ExceptionHandler에서 처리 가능
  • 참고
    • https://supawer0728.github.io/2018/04/04/spring-filter-interceptor/
    • https://goddaehee.tistory.com/154

Filter 예제

설명

  • ServletComponentScan + WebFilter 조합으로 구현시 필터 체인의 순서를 정의할 수 없음
  • Component + Order 조합으로 구현시 url pattern을 정의할 수 없음
  • 고전적인 방법이 제일 좋아보임

RequestLoggingFilter

@Slf4j
public class RequestLoggingFilter extends HttpFilter {
    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("logging - before");
        chain.doFilter(request, response);
        log.info("logging - after");
    }
}

AuthFilter

@Slf4j
public class AuthFilter extends HttpFilter {
    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("auth - before");

        if (StringUtils.equals(request.getRequestURI(), "/api/post")) {
            response.sendRedirect("/api/error");
            return;
        }

        // doFilter()를 호출하지 않으면 다음 필터 or 컨트롤러를 수행하지 않음
        chain.doFilter(request, response);
        log.info("auth- after");
    }
}

ApiFilterConfig

@Configuration
public class ApiFilterConfig {
    @Bean
    public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
        return apiFilter(new RequestLoggingFilter(), 1);
    }

    @Bean
    public FilterRegistrationBean<AuthFilter> authFilter() {
        return apiFilter(new AuthFilter(), 2);
    }

    private <T extends Filter> FilterRegistrationBean<T> apiFilter(T filter, int order) {
        FilterRegistrationBean<T> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.addUrlPatterns("/api/*");
        registrationBean.setOrder(order);
        return registrationBean;
    }
}

Interceptor 예제

TestInterceptor

public class TestInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        HandlerMethod handlerMethod = (HandlerMethod) handler; // handlerMethod를 통해 컨트롤러 정보 활용 가능
        MyAnnotation myAnnotation = handlerMethod.getMethodAnnotation(MyAnnotation.class); // 메소드 어노테이션 활용하는 예시

        // Controller 실행 전 동작
        // 반환 값이 false일 경우 Controller 진입하지 않음
        // Object handler는 Controller 객체
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        // Controller 실행 후 동작
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // View 렌더링 완료 후 실행
    }
}

WebMvcConfig

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TestInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login/**");
    }
}

Filter에서 Access Log 찍기

Request, Response를 doFilter() 이후에 로그 찍기

@Getter
@Builder
public class Log {
    private final String status;
    private final String method;
    private final String url;
    private final Map<String, Object> parameters;
    private final Map<String, Object> headers;
    private final String body;

    public static Log of(HttpServletRequest request, String body) {
        return Log.builder()
            .status("0")
            .method(request.getMethod())
            .url(request.getRequestURI())
            .parameters(
                request.getParameterMap().entrySet().stream()
                    .collect(Collectors.toMap(
                        entry -> entry.getKey(),
                        entry -> getValue(Arrays.asList(entry.getValue())))
                    )
            )
            .headers(
                Collections.list(request.getHeaderNames()).stream().distinct()
                    .collect(Collectors.toMap(
                            name -> name,
                            name -> getValue(Collections.list(request.getHeaders(name)))
                        )
                    )
            )
            .body(body)
            .build();
    }

    public static Log of(HttpServletResponse response, String body, String method, String url) {
        return Log.builder()
            .status(String.valueOf(response.getStatus()))
            .method(method)
            .url(url)
            .parameters(Collections.emptyMap())
            .headers(
                response.getHeaderNames().stream().distinct()
                    .collect(Collectors.toMap(
                        name -> name,
                        name -> getValue(new ArrayList<>(response.getHeaders(name)))
                    ))
            )
            .body(body)
            .build();
    }

    private static Object getValue(List<String> values) {
        return values.size() == 1 ? values.get(0) : values;
    }
}
/**
 * implementation("commons-io:commons-io:2.15.0")
 */
@Slf4j
@RequiredArgsConstructor
@Component
public class AccessLoggingFilter extends OncePerRequestFilter {
    private final ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        // doFilter() 수행된 이후에 getContentAsByteArray()를 호출해야 body 로깅이 가능
        filterChain.doFilter(requestWrapper, responseWrapper);

        if (log.isInfoEnabled()) {
            String requestBody = IOUtils.toString(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8.name());
            log.info("[ACCESS_LOG:REQUEST] {}", objectMapper.writeValueAsString(Log.of(request, requestBody)));
            
            String responseBody = IOUtils.toString(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8.name());
            log.info("[ACCESS_LOG:RESPONSE] {}", objectMapper.writeValueAsString(Log.of(response, responseBody, request.getMethod(), request.getRequestURI())));
        }

        // 아래 코드를 호출하지 않으면 responseBody가 Empty로 응답
        responseWrapper.copyBodyToResponse();
    }
}

Request는 doFilter() 호출 전에 찍고 싶을 경우

public class EagerContentCachingRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;

    @SneakyThrows
    public EagerContentCachingRequestWrapper(HttpServletRequest request) {
        super(request);
        this.body = IOUtils.toByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() {
        return new ServletInputStream() {
            private final ByteArrayInputStream in = new ByteArrayInputStream(body);

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener listener) {
                throw new UnsupportedOperationException();
            }

            @Override
            public int read() {
                return in.read();
            }
        };
    }
}
/**
 * implementation("commons-io:commons-io:2.15.0")
 */
@Slf4j
@RequiredArgsConstructor
@Component
public class AccessLoggingFilter extends OncePerRequestFilter {
    private final ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.getParameterNames(); // 호출해주지 않으면 application/x-www-form-urlencoded 요청시 body의 정보가 parameter로 변환되지 않음.
        
        EagerContentCachingRequestWrapper requestWrapper = new EagerContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        if (log.isInfoEnabled()) {
            String requestBody = IOUtils.toString(requestWrapper.getInputStream(), StandardCharsets.UTF_8);
            log.info("[ACCESS_LOG:REQUEST] {}", objectMapper.writeValueAsString(Log.of(request, requestBody)));
        }

        // doFilter() 수행된 이후에 getContentAsByteArray()를 호출해야 body 로깅이 가능
        filterChain.doFilter(requestWrapper, responseWrapper);

        if (log.isInfoEnabled()) {
            String responseBody = IOUtils.toString(responseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8.name());
            log.info("[ACCESS_LOG:RESPONSE] {}", objectMapper.writeValueAsString(Log.of(response, responseBody, request.getMethod(), request.getRequestURI())));
        }

        // 아래 코드를 호출하지 않으면 responseBody가 Empty로 응답
        responseWrapper.copyBodyToResponse();
    }
}

참고

  • https://cnpnote.tistory.com/entry/SPRING-Spring-Boot-한-곳에서-모든-요청과-응답을-예외로-기록하는-방법
  • https://www.sollabs.tech/ContentCachingRequestWrapper
반응형

'Development > Spring' 카테고리의 다른 글

[Spring] Swagger  (0) 2020.12.27
[Spring] Argument Resolver  (0) 2020.12.27
[Spring] Validation  (0) 2020.12.27
[Spring] BeanFactory & ApplicationContext  (0) 2020.12.27
[Spring] Paging  (0) 2020.12.27

+ Recent posts