BackEnd/Spring MVC

[Spring] 로그인 처리2 - 필터, 인터셉터 - 2

샤아이인 2022. 3. 14.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

3. 스프링 인터셉터 - 소개

스프링 인터셉터는 서블릿 필터와 비슷하지만, 스프링 MVC가 제공하는 기술이다.

 

● 스프링 인터셉터의 흐름

HTTP 요청 ->WAS-> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
 

인터셉터는 프론트 컨트롤러(DispatcherServlet) 과 컨트롤러 사이에서 호출된다.

스프링 인터셉터에도 URL 패턴을 적용할 수 있는데, 서블릿 URL 패턴과는 다르고, 서블릿 보다 매우 정밀하게 설정할 수 있다.

 

● 스프링 인터셉터의 제한

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비 로그인 사용자
 

위와 같이 로그인 여부를 체크하기에도 좋다.

 

또한 인터셉터는 체인으로 구성되어, 여러 체인을 추가할 수 있다.

 

● 스프링 인터셉터의 Interface

public interface HandlerInterceptor {

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

    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 {}
}
 

서블릿 필터의 경우 doFilter()에서 대부분의 로직을 처리했지만, 인터셉터는 3가지를 제공한다.

컨트롤러 호출 전( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion )

 

인터셉터는 어떤 컨트롤러( handler )가 호출되는지 호출 정보도 받을 수 있다.

그리고 어떤 modelAndView 가 반환되는지 응답 정보도 받을 수 있다.

출처 - 인프런 김영한 스프링 MVC2

● 정상 흐름

- preHandle : 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.)

preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않는다.

false 인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 위 그림에서 1번에서 끝이 나버린다.

 

- postHandle : 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.)

컨트롤러에서 예외가 발생하면 호출되지 않는다.

 

- afterCompletion : 뷰가 렌더링 된 이후에 호출된다. (예외가 발생해도 호출된다.)

예외가 발생하면 postHandle() 는 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion() 을 사용해야 한다.

예외가 발생하면 afterCompletion() 에 예외 정보( ex )를 포함해서 호출된다.

 

4. 스프링 인터셉터 - 요청 로그

사용자가 요청을 보내올때 마다 로그를 남겨보자!

 

● LogInterceptor - 요청 로그 인터셉터

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

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

        request.setAttribute(LOG_ID, uuid);

        if(handler instanceof HandlerMethod){
            HandlerMethod hm = (HandlerMethod) handler;
        }

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

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

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

- UUID

이전과 같이 요청 로그상에서 사용자를 구분하기 위해 UUID를 생성하였다.

 

- request.setAttribute()

서블릿 필터에서는 지역변수로 데이터를 공유하였지만, 스프링 인터셉터는 각각의 호출시점이 완전히 분리되어 있기 때문에 힘들다.

따라서 preHandle 에서 지정한 값을 postHandle , afterCompletion 에서 함께 사용하려면 어딘가에 담아두어야 하는데 이때 request를 사용한다.

request에 담아둔 값은 afterCompletion 에서 request.getAttribute(LOG_ID) 로 찾아서 사용한다.

 

또한 LogInterceptor 도 싱글톤 처럼 사용되기 때문에 맴버변수를 사용하여 공유하기에는 위험하다.

 

- return true

true를 반환하면 정상 호출로 인식되어, 다음 인터셉터나 컨트롤러를 호출한다.

 

 

● HandlerMethod

preHandle()는 인자로 handler를 받는데, 이를 활용할수 가 있다.

if (handler instanceof HandlerMethod) {
    HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다. 
}
 

핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다.

스프링을 사용하면 일반적으로 @Controller , @RequestMapping 을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod 가 넘어온다.

 

● 인터셉터 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
    }

    // 생략...
}
 

WebMvcConfigurer 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다.

필터와 비교해보면 인터셉터는 addPathPatterns , excludePathPatterns 로 매우 정밀하게 URL 패턴을 지정할 수 있다.

 

실핼 결과는 다음과 같다.

 

5. 스프링 인터셉터 - 인증 체크

이전에 서블릿 필터로 개발한 인증 체크 기능을 스프링의 인터셉터로 구현해 보자!

 

● LoginCheckInterceptor

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

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

        log.info("인증 체크 인터셉터 실행 {}", requestURI);
        HttpSession session = request.getSession();

        if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
            log.info("미인증 사용자 요청");
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }

        return true;
    }
}
 

매우 간단해졌다. 인증은 컨트롤러 호출 전에만 진행되면 되기 때문에 preHandler를 구현한 상태이다.

또한 기존의 whiteList와 같은 어느 요청을 통과시키고, 인증처리를 할지 정하는 부분이 없다.

이는 WebConfig에 등록할때 설정할수 있다.

 

● WebConfig - 설정하기

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");

        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/add", "/login", "/logout", "/css/**", "/*.ico", "/error");
    }
}
 

인터셉터를 적용하거나 하지 않을 부분은 addPathPatterns 와 excludePathPatterns 에 작성하면 된다.

 

모든 경로( /** )에 해당 인터셉터를 적용하되, 홈( / ), 회원가입( /members/add ), 로그인( /login ), 리소스 조회( /css/** ), 오류( /error )와 같은 부분은 로그인 체크 인터셉터를 적용하지 않는다.

 

기존의 서블릿 필터와 비교해보면 매우 편리한 것을 알 수 있다. 따라서 일반적으로 인터셉터를 더 자주 사용하게 된다.

댓글