반응형

Filter, Interceptor 란?

  • Filter와 Interceptor은 사용자가 특정 URL에 접근, 요청 시 해당 요청에 대한 응답을 하기 전에 요청을 가로채 확인하고 통과 시킬지 말지를 결정할 수 있게 하는 역할
  • 어떤 사용자가 특정 URL에 접근 시 해당 요청에 대한 로그를 남겨야 하는 상황이나 로그인 한 사용자(인증) 혹은 특정 권한이 있는 사용자(인가)만 특정 URL에 접근할 수 있는 상황 등에 사용할 수 있음

Filter와 Interceptor의 차이

  • Filter와 Interceptor의 사용 목적은 비슷하지만 차이점이 존재
  1. Filter은 JAVA에서 제공하는 기능이지만, Interceptor은 Spring에서 제공하는 기능
    • Filter은 Spring과 무관하게 전역적으로 처리하는 작업들을 수행할 때 사용할 수 있음
    • Interceptor은 Controller(Handler)에 관한 요청과 응답에 대해 처리할 때 사용할 수 있음
  2. Filter와 Interceptor은 실행되는 위치가 다름
    • HTTP 요청 -> WAS -> Filter -> Spring의 Dispatcher Servlet -> Spring Interceptor -> Controller
      • Filter Chain과 Interceptor Chain을 사용하면 여러개의 Filter, Interceptor 사용 가능
      • ex) HTTP 요청 -> WAS -> Filter1 -> Filter2 -> Filter3 -> Dispatcher Servlet -> Interceptor1 -> Interceptor2 -> Controller
  3. Filter은 Dispatcher Servlet 앞에서 동작하기 때문에 ServletRequest, ServletResponse를 받아서 처리하지만, Interceptor은 HttpServletRequest, HttpServletResponse를 받아서 처리
  4. ex) 인증 기능 구현 시
    • Filter은 인증에 성공하면 권한을 부여하고 다음 Filter로 진행 (진행하지 않을 수도 있음)
    • Interceptor은 인증에 실패하면 다음으로 진행 X
  5. Interceptor은 Spring에서 제공하는 기능이기 때문에 스프링 MVC 구조에 조금 더 특화되었음
    • Interceptor에서는 어떤 Controller(Handler)가 호출 되었는지에 대한 호출 정보도 받을 수 있고, modelAndView도 받을 수 있음
    • Interceptor의 afterCompletion을 사용해 예외가 발생해도 호출하는 부분이 있어 예외를 따로 처리할 수 있음

Filter 예제

  • Filter는 3개의 메서드를 가짐
  1. init()
    • Filter 초기화 메서드
    • Servlet Container Filter을 Singleton 객체로 생성하고 관리
  2. doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    • 사용자의 요청이 올때마다 해당 메서드 호출
    • 여기에 필터의 로직을 구현하면 됨
  3. destroy()
    • Filter 종료 메서드
    • Servlet Container가 종료될 때 호출
  • chain.doFilter(request, response)를 해줘야 다음 필터로 이동

LogFilter 예제

  • 사용자의 모든 요청에 대한 로그를 Filter를 사용해 기록해보는 예제

LogFilter 생성

@Slf4j
public class LogFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("Log Filter : init");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("Log Filter : doFilter 실행");

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String requestURI = httpServletRequest.getRequestURI();

        // 요청의 추적을 위해 UUID 사용
        String uuid = UUID.randomUUID().toString();
        request.setAttribute("logId", uuid);

        try {
            log.info("Log Filter : doFilter : REQUEST [{}][{}]", uuid, requestURI);
            chain.doFilter(request, response);
        } catch (Exception e) {
            throw e;
        } finally {
            log.info("Log Filter : doFilter : RESPONSE [{}][{}]", uuid, requestURI);
        }
    }

    @Override
    public void destroy() {
        log.info("Log Filter destroy");
    }
}

LogFilter 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();

        // 구현한 필터 등록
        filterFilterRegistrationBean.setFilter(new LogFilter());

        // 필터 순서 지정
        filterFilterRegistrationBean.setOrder(1);

        // 필터가 적용될 URL 지정
        filterFilterRegistrationBean.addUrlPatterns("/filter-test/pass");


        return filterFilterRegistrationBean;
    }
}

FilterController 생성

@Slf4j
@Controller
@RequestMapping("/filter-test")
public class FilterController {

    @GetMapping("/all-pass")
    public String allPass() {
        log.info("all-pass 호출");
        return "filterPassPage";
    }

    @GetMapping("/pass")
    public String pass(HttpServletRequest request) {
        // Log Filter에서 넣어준 UUID 꺼내와서 출력
        log.info("FilterController : REQUEST [{}][{}]", request.getAttribute("logId"), request.getRequestURI());
        return "filterPassPage";
    }
}

결과

  • /filter-test/pass로 두 번 요청 후 /filter-test/all-pass로 요청 시 결과

  • /filter-test/pass로 요청 시 Filter을 거치기 때문에 LogFilter에서 로그가 찍힌것을 확인할 수 있음
  • 두 번의 요청의 UUID가 다름 => 이를 활용하여 하나의 요청 추적 가능
  • /filter-test/all-pass로 요청 시 Filter을 거치지 않기 때문에 LogFilter에 들리지 않는것을 확인할 수 있고 logId가 null인 것도 확인할 수 있음

Interceptor 예제

  • Interceptor는 3개의 메서드를 가짐
  1. preHandle()
    • Dispatcher Servlet 호출 이후 Controller 호출 직전에 호출
    • 로그인 기능 구현시 이 메서드에서 사용자 권한 체크 후 권한이 없으면 Controller를 호출하지 않으면 됨
  2. postHandle()
    • Controller 호출 이후 View 렌더링 전에 호출
    • 만약 Controller에서 Exception이 발생한다면 postHandle은 실행되지 않음
    • 이 메서드는 Controller에서 받은 ModelAndView를 출력하는 용도 등으로 사용 가능
  3. afterCompletion()
    • View가 렌더링 된 이후에 호출
    • afterCompletion은 Exception이 발생해도 호출되기 때문에 exception에 대한 로그를 찍고 싶은 상황 등에 사용 가능

LogInterceptor 예제

  • 사용자의 모든 요청에 대한 로그를 Interceptor를 사용해 기록해보는 예제
  • uuid를 사용하여 로그를 추적하는 기능은 Filter와 같으니 Interceptor에서는 구현 생략

LogInterceptor 생성

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();

        log.info("preHandle : REQUEST [{}][{}]",  requestURI, handler);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle : RESPONSE [{}]", modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String requestURI = request.getRequestURI();
        log.info("afterCompletion : RESPONSE [{}][{}]", requestURI, handler);

        if(ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}

LogInterceptor 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/interceptor-test/pass")
                .excludePathPatterns("/interceptor-test/all-pass");
    }
}
  • /interceptor-test/pass는 LogInterceptor을 통과
  • /interceptor-test/all-pass는 LogInterceptor 통과 X

LogController 생성

@Slf4j
@Controller
@RequestMapping("/interceptor-test")
public class InterceptorController {

    @GetMapping("/all-pass")
    public String allPass(HttpServletRequest request) {
        log.info("REQUEST [{}]", request.getRequestURI());
        return "interceptorPassPage";
    }

    @GetMapping("/pass")
    public String pass(HttpServletRequest request, Model model) {
        log.info("REQUEST [{}]", request.getRequestURI());
        model.addAttribute("Key1", "Value1");
        model.addAttribute("Key2", "Value2");
        return "interceptorPassPage";
    }
}

결과

  • /interceptor-test/pass로 요청 후 /interceptor-test/all-pass로 요청 시 결과

  • /interceptor-test/pass로 요청 시 Interceptor을 거치기 때문에 LogInterceptor에서 로그가 찍힌것을 확인할 수 있음
  • preHandle()에서는 해당 요청을 처리하는 Handler(InterceptorController)을 알 수 있음
  • postHandle()에서는 해당 요청의 Response에 대한 modelAndView를 확인할 수 있음
  • afterCompletion()에서도 preHandle()과 마찬가지로 해당 요청을 처리하는 Handler을 알 수 있고, Exception이 발생한다면 exception을 출력해 볼 수도 있음
  • /interceptor-test/all-pass로 요청 시 Interceptor을 거치지 않기 때문에 LogInterceptor에 들리지 않는것을 확인할 수 있음

Filter와 Interceptor을 사용한 인증 기능 구현 예제

  • 이 예제에서는 로그인 기능을 제대로 구현하지는 않음
    • 하지만 이 예제를 활용해 로그인 기능을 구현할 수 있음
  • 만약 로그인이 성공했다면 Request의 Header에 "pass" 값이 true이고, 실패했다면 Header에 "pass"값이 없거나 false라고 가정한 상황
  • Filter와 Interceptor에서 request.getHeader("pass")를 사용해 인증 기능을 구현해 보자
    • 로그인 기능 구현을 하려면 Header에 Token을 넣어 전송하거나, Cookie, Session 등의 방법 활용하면 됨

AuthFilter 생성

@Slf4j
public class AuthFilter implements Filter {

    // 권한 인증을 체크 하지 않을 URL
    private static final String[] whiteList = {"/filter-test/all-pass", "/filter-test/fail"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String requestURI = httpServletRequest.getRequestURI();

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        // 요청의 추적을 위해 UUID 사용
        String uuid = UUID.randomUUID().toString();
        request.setAttribute("logId", uuid);

        try {
            log.info("doFilter : REQUEST [{}][{}]", uuid, requestURI);

            // requestURI가 Auth Check가 필요한 요청이라면 체크
            if(isAuthCheckPath(requestURI)) {
                log.info("doFilter : 인증 체크 로직 실행 REQUEST [{}][{}]", uuid, requestURI);

                boolean auth = Boolean.valueOf(httpServletRequest.getHeader("pass"));

                if(auth != true) {
                    log.info("dofilter : 인증 실패 : REQUEST [{}][{}]", uuid, requestURI);
                    httpServletResponse.sendRedirect("/filter-test/fail");
                    return;
                }
            }

            log.info("dofilter : 인증 성공 : REQUEST [{}][{}]", uuid, requestURI);
            chain.doFilter(request, response);

        } catch (Exception e) {
            throw e;
        } finally {
            log.info("dofilter : RESPONSE [{}][{}]", uuid, requestURI);
        }

    }

    private boolean isAuthCheckPath(String requestURI) {
        return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
    }
}
  • 이렇게 생성한 Filter은 LogFilter와 같이 등록해주면 사용 가능
  • whiteList와 isAuthCheckPath 메서드를 사용해 검증이 필요한 URL 인지 체크하기 때문에 Filter을 등록할 때, 모든 URL이 AuthFilter을 거치게 해도 됨
  • FilterController을 그대로 사용하되 로그 출력 형식만 조금 수정

결과

  • AuthFilter을 거치지 않는 URL

  • AuthFilter을 거치는 URL => 인증 실패
    • /filter-test/pass에 요청 -> 인증 실패 -> /filter-test/fail로 redirect -> 이 요청은 인증이 필요 없음 -> 인증 성공 -> filterFailPage Return

  • AuthFilter을 거치는 URL => 인증 성공

AuthInterceptor 생성

@Slf4j
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        request.setAttribute("logId", uuid);


        boolean auth = Boolean.valueOf(request.getHeader("pass"));

        if(auth != true) {
            log.info("preHandle : 인증 실패 : REQUEST [{}][{}]", uuid, requestURI);
            response.sendRedirect("/interceptor-test/fail");
            return false;
        }

        log.info("preHandle : 인증 성공 : REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle : RESPONSE [{}][{}]", request.getAttribute("logId"), modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String requestURI = request.getRequestURI();
        String logId = (String) request.getAttribute("logId");
        log.info("afterCompletion : RESPONSE [{}][{}][{}]", logId, requestURI, handler);

        if(ex != null) {
            log.error("afterCompletion error!!", ex);
        }
    }
}
  • 이렇게 생성한 Interceptor은 LogInterceptor와 같이 등록해주면 사용 가능
  • Authinterceptor은 AuthFilter와 달리 .addPathPatterns로 인증이 필요한 URL을 따로 등록해 주었음

결과

  • AuthInterceptor을 거치지 않는 URL

  • AuthInterceptor을 거치는 URL => 인증 실패

  • AuthInterceptor을 거치는 URL => 인증 성공

Filter, Interceptor 동시 적용 예제

  • 위에서 만들어놓은 LogFilter, LogInterceptor을 동시에 적용하는 URL을 만들어 순서를 확인해보는 예제
  • URL 부분만 수정해주면 되기 때문에 코드 생략

결과

  • 실행 순서 : Filter의 init() -> Filter의 doFilter() try 부분 -> Interceptor의 preHandle() -> Controller -> Interceptor의 postHandle() -> Interceptor의 afterCompletion() -> Filter의 doFilter() finally 부분 -> Filter의 destroy()
  • Filter와 Interceptor에서 저장한 logId는 각자 따로 사용

반응형

↓ 클릭시 이동

복사했습니다!