HandlerInterceptor & Filter / SecurityFilterChain

2024. 1. 27. 09:54
728x90

1. HandlerInterceptor & Filter

- 핵심 기능을 실행하는데 반드시 필요하지는 않지만, 여러 핵심기능에 걸쳐서 동일하게 작동하는 기능이 존재할 수 있다. (ex. 한 메서드가 실제로 실행하는데 얼마나 시간이 걸렸는지 확인하는 메서드.)

 
- 핵심 기능 관리 코드는 건드리지 않으면서 여러 핵심 기능들이 활용할 수 있는 기능! >> 요청과 응답의 전후로 코드를 실행할 수 있는 방법 >> 이것이 바로 HandlerInterceptor & Filter
 

1) HandlerInterceptor : Spring Framework의 일부분.
- DispatcherServlet이 HandlerMethod로 요청을 넘기기 전에 실행. = 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나, 가공할 수 있는 기능을 제공. 
- 비즈니스 로직과 연관된 기능 구현에 사용.
 
2) Filter : Jakarta Servlet API의 일부분.
- DispatcherServlet에 도달하기 전에 요청을 검사. 
- Spring 외부의 기능이므로 > 예외처리 등 Spring Framework의 도움을 받지 못한다. (Web Container에 의해 관리됨)
- 비즈니스 로직과 무관한 기능 구현

 

1) HandlerInterceptor class

- 디스패처 서블릿이 HandlerMapping을 통해 적절한 컨트롤러를 찾는다. >> 이 결과로 실행 체인(HandlerExecutionChain)을 반환한다. 
*디스패처 서블릿 관련한 글은 👇여기서 정리했다.

- 무튼 이 HandlerExecutionChain은 인터셉터가 등록되어 있으면 > 순차적으로 인터셉터들을 거쳐 컨트롤러가 실행되도록 하고,
없다면 > 바로 컨트롤러를 실행한다. 
 

** HandlerInterceptor 인터페이스의 메서드 3가지

public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, 
                              HttpServletResponse response, 
                              Object handler
    ) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler,
                            @Nullable ModelAndView modelAndView
    ) throws Exception {}

    default void afterCompletion(HttpServletRequest request, 
                                 HttpServletResponse response, 
                                 Object handler,
                                 @Nullable Exception ex
    ) throws Exception {}

}

- preHandle : HandlerMethod 실행 전(즉 컨트롤러가 호출되기 전) 실행되는 메서드

  • preHandle에서 false를 반환하면 요청이 전달되지 않는다.
  • handler 파라미터 : HandlerMapping이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod라는 새로운 타입의 객체이다. (@RequestMapping이 붙은 메서드의 정보를 추상화한 객체)

- postHandle : HandlerMethod 실행 후(즉 컨트롤러가 호출된 후), 응답이 전달되기 전 실행되는 메서드.

  • 추가로 View를 전달한다면 View의 내부를 채워넣기 전에 실행되는 메서드. (ModelAndView타입의 정보)
  • 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하면 postHandle은 호출되지 않는다.

- afterCompletion : 요청의 처리가 마무리된 뒤에 실행되는 메서드.

  • postHandle과 달리, 예외가 발생했다면 예외의 정보가 인자로 추가됨. 
  • 그래서 예외가 발생하더라도 afterCompletion은 반드시 호출된다.

 
ex) 어떤 header가 있었는지를 로그로 남기는 인터셉터 만들어보기
1) HandlerInterceptor를 implements하는 인터셉터 만들기.

@Component
@Slf4j
public class HeaderLoggingInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, 
                             Object handler 
    ) throws Exception {
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

 
2) preHandle
: HandlerMethod는 메서드와 빈을 포함해 handler method에 대한 정보를 캡슐화하는 객체이다! 얘로 getMethod의 getName을 하여 어떤 요청인지 로깅 가능.

// HeaderLoggingInterceptor class
	@Override
    public boolean preHandle(HttpServletRequest request,//요청
                             HttpServletResponse response, //응답
                             Object handler //실제로 요청을 처리할 RequestMapping을 나타내는 메서드 객체
    ) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler; //Object handler 객체를 HandlerMethod로 형변환
        //어떤 요청인지 로깅하기
        log.info("pre handling of {}", handlerMethod.getMethod().getName());
        
        // request의 headerName들을 받고 그 headerName으로 header를 받아서 로깅하기
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            log.info("{}: {}", headerName, request.getHeader(headerName));
        }
        //여기까지 preHandle이라고 로깅하기.
        log.info("=========================================end of pre handling");
        
        // false를 반환하면 요청이 전달되지 않으므로 true~
        return true;
    }

 
 
 
2) postHandle

// HeaderLoggingInterceptor class
	@Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView
    ) throws Exception {
        //Object handler 객체를 HandlerMethod로 형변환
        HandlerMethod handlerMethod = (HandlerMethod) handler;

        //어떤 요청인지 로깅하기
        log.info("post handling of {}", handlerMethod.getMethod().getName());

        //response의 headerNames를 받고 그 headerName으로 header 받아서 로깅하기
        Collection<String> headerNames = response.getHeaderNames();
        for (String headerName : headerNames) {
            log.info("{}: {}", headerName, request.getHeader(headerName));
        }

        //여기까지 postHandle이라고 로깅하기
        log.info("=========================================end of post handling");
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

 
 
3) afterCompletion

// HeaderLoggingInterceptor class
	@Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        log.info("afterCompletion :: Bye!");
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

 
4) Configuration를 통해 실행하도록 설정한다. 
: WebMvcConfigurer을 상속받아 addInterceptors 메서드를 오버라이딩한다. > 여기서 interceptor를 등록

// InterceptorConfig class
@Configuration
@RequiredArgsConstructor
public class InterceptorConfig implements WebMvcConfigurer {
    private final HeaderLoggingInterceptor loggingInterceptor;

    @Override
    // 인터셉터를 등록하기 위한 메서드.
    public void addInterceptors(InterceptorRegistry registry) {
                // 어떤 인터셉터를
        registry.addInterceptor(loggingInterceptor)
                //어떤 경로에
                .addPathPatterns("/tests");
    }
}

 
5) Controller는 이런 모습이다.

@RestController
@Slf4j
@RequestMapping("/tests")
public class TestController {

    @GetMapping
    public String test() {
        return "done";
    }

    @PostMapping
    public String testBody(
            @RequestBody
            TestDto dto
    ) {
        log.info(dto.toString());
        return "dooooooone";
    }

}

 
결과는 이렇게 나온다~ preHandle 부터 postHandle, afterCompletion까지~~~!! 신기하네

 
 

2) Filter class

- Filter(jakarta.servlet) 인터페이스 구현 (import시 주의하기)
- Filter 인터페이스를 구현한 필터를 만들면 된다. SpringBoot는 Filter 인터페이스를 구현하고, Bean 객체로 등록하면 Filter를 자동으로 사용하게 된다.
- Servlet API를 바로 활용하기 때문에 내부에서 ServletRequest나 ServletResponse를 직접 조작 가능 (그러나 Spring에서 제공하는 편의 기능을 활용하기 어렵다)
- doFilter() 오버라이딩
 

** Filter interface의 3가지 메서드

package jakarta.servlet;

import java.io.IOException;

public interface Filter {

    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest request, 
                  ServletResponse response, 
                  FilterChain chain)
            throws IOException, ServletException;

    default void destroy() {
    }
}

- init : 필터 객체를 초기화하고 서비스에 추가하기 위한 메서드. 웹 컨테이너가 1회 init 메서드를 호출하여 필터 객체를 초기화하면 이후의 요청들은 doFilter를 통해 처리된다.
- doFilter : url-pattern에 맞는 모든 HTTP 요청이 > 디스패처 서블릿으로 전달되기 전 > 웹 컨테이너에 의해 실행되는 메서드. 

  • 파라미터로 FilterChain이 있는데, FilterChain의 doFilter를 통해 다음 대상으로 요청을 전달한다!! > 원하는 처리를 원하는 순간에 해줄 수 있다.

- destroy : 필터 객체를 서비스에서 제거하고, 사용하는 자원을 반환하기 위한 메서드. 웹 컨테이너가 1회 호출하며 이후에는 doFilter에 의해 처리되지 않는다. 
 
ex. 요청과 응답을 로깅하는 Filter
- @Component를 해주면 모든 url에 적용
- @WebFilter(urlPatterns = <url>) + @ServletComponentScan :: 순서 지정이 어렵다
- FilterRegistrationBean으로 등록하기 (가장 옵션이 많다)

package com.example.auth.filters;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
public class LoggingFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain
    ) throws IOException, ServletException {
        // ServletRequest 요청을 HttpServletRequest로 형변환
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        // request 시작 : 어떤 HttpMethod로 요청이 왔는지, 어떤 URI로 요청이 왔는지 로깅하기
        log.info("start requests: {} {}",
                httpServletRequest.getMethod(),
                httpServletRequest.getRequestURI());

        // FilterChain의 doFilter로 다음 필터에 체이닝하기 (호출하지 않을 경우 다음 필터가 실행되지 않음)
        chain.doFilter(request, response);

        // -- 이 아래는 요청 처리 후
        // 요청 처리 후에는 Response 보여주기

        // ServletResponse 응답을 HttpServletResponse로 형변환
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        log.info("send response : {}", httpServletResponse.getStatus());
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

 

>> 결과는 이렇다. 맨 위/맨 아래만 Filter logging이다!! 
요청의 HTTP Method 잘 나왔고, 어떤 URI로 왔는지 잘 나왔다.
응답의 HTTP Status도 잘 나왔다~
오예!


2. SecurityFilterChain

- Spring Security는 Filter를 이용해 그 기능을 제공한다. 

- 어떤 클라이언트의 요청에 대해서 구성된 Filter들을 담고 있는 FilterChain 객체를 통해 순차적으로 사용한다. (FilterChain의 doFilter(request, response)를 통해 연결)

- 기본적인 Servlet Filter는 Spring Application과 별도이기 때문에 > Bean 객체를 찾지 못한다. 
>> 그래서 Spring에서는 DelegatingFilterProxy를 활용해 Bean 객체를 찾을 수 있는 Filter를 만들게 해준다. 

- Spring Security는 DelegatingFilterProxy를 활용해, FilterChainProxy를 등록한다.
FilterChainProxy에는 다시 우리가 구성한 SecurityFilterChain이 등록된다. 
- 이 SecurityFilterChain에 우리가 만든 Filter를 등록하고, 인증을 진행할 수 있다. 
 
>> 너무 길어져서 이것에 대해서는 더 자세히 다음에 다루겠다! 
 
 
 

728x90

'Programming > Spring, SpringBoot' 카테고리의 다른 글

Authorization  (0) 2024.01.30
JWT  (1) 2024.01.29
Spring의 Dispatcher Servlet  (3) 2024.01.26
CustomUserDetailsManager과 CustomUserDetails 만들기  (1) 2024.01.26
Spring Security... 그리고 인터페이스  (0) 2024.01.25

BELATED ARTICLES

more