ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC - interceptor를 이용한 요청 로그 기록과 로그인 체크
    Spring/Spring MVC 2022. 2. 9. 16:47
    반응형

     

     

     

     

     

    interceptor를 이용한 요청 로그 기록과 로그인 체크

     

     

     웹 페이지에서는 특정 사용자에게만 보여주는 페이지 또는 로그인을 한 사용자에게만 보여주는 페이지 등 URL에 따라 접근할 수 있는 조건을 체크해줘야 한다. 만약 URL의 접근 조건을 체크해주지 않으면 일반 사용자가 관리자 페이지로 들어갈 수 있거나 로그인을 하지 않는 사용자가 로그인한 사용자에게만 보이는 페이지(정보 수정 등)의 페이지로 들어갈 수 있는 등의 문제가 발생한다. 

     

     따라서 특정 페이지의 조건을 체크해줘야 하는데 컨트롤러에 조건을 체크하는 로직을 하나하나 작성하면 코드 중복이 발생할 뿐만 아니라 나중에 유지 보수할 때 해당 로직을 일일이 찾아서 수정해야 하는 번거로움이 생긴다.

     

     특정 페이지의 조건을 체크하는 공통의 관심사(Cross-Cutting Concern)를 해결해기 위해서 서블릿 필터 또는 스프링 인터셉터를 사용한다. 웹과 관련된 공통 관심사들은 HttpServletRequest가 제공하는 HTTP의 헤더와 URL의 정보들이 필요하기 때문에 서블릿 필터 스프링 인터셉터를 사용한다.

     

     참고로 스프링 MVC를 사용할 때 서블릿 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다


     

    interceptor

     스프링 인터셉터는 스프링 MVC가 제공하는 기능으로서 서블릿 필터와 같이 웹과 관련된 공통 로직 처리를 효과적으로 해결할 수 있다. 

     

     

    인터셉터 흐름

     

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

     

     

    • 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 호출된다.
    • 스프링 MVC가 제공하는 기능이기 때문에 디스패처 이후에 등장
      ( 스프링 MVC의 시작점이 디스패처 서블릿 )
    • 스프링 인터셉터는 서블릿 필터와는 URL 패턴을 보다 매우 정밀하게 설정할 수 있다.

     

    인터셉터를 이용한 요청 제한

     

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

     

     인터셉터 또한 적절하지 않은 요청이라 판단하면 인터셉터에서 해당 컨트롤러를 호출하지 않고 끝낼 수 있다.


     

    인터셉터 체인

     

    HTTP 요청 → WAS → 필터 → 서블릿 → 인터셉터1 → 인터셉터2 → 컨트롤러

     

     인터셉터도 필터와 마찬가지로 우선순위에 따른 인터셉터 처리 순서를 정하여 자유롭게 추가할 수 있다.


     

    인터셉터 인터페이스

     

    • preHandle() 
           - 컨트롤러 호출 전에 호출
           - return 값이 ture면 다음 인터셉터or컨트롤러 호출, false면 더이상 진행하지 않는다.
    • postHandle 
           - 컨트롤러 호출 후에 호출
           - 컨트롤러에서 예외 발생 시 호출되지 않는다.
    • afterCompletion
           - 뷰가 렌더링 된 이후에 호출
           - 컨트롤러에서 예외 발생하여도 호출된다.

     

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


     

     

    Interceptor를 이용한 요청 로그 기록

     

    로그 인터셉터 구현

     

    public class LogInterceptor 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(LOG_ID, uuid);
    
        //@RequestMapping: HandlerMethod
        //정적 리소스: ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;//호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
        }
    
        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
        return true;
    }

    ▶ 컨트롤러가 호출되기 전 로그 출력을 위해 호출된 메서드

    String uuid = UUID.randomUUID().toString();
             - 요청 로그를 구분하기 위한 uuid 생성

    request.setAttribute(LOG_ID, uuid)
             - 필터와는 달리 postHandle, afterCompetion에서 사용하기 위해서 request영역에 저장
             - 필터의 경우 한 메서드에서 다음 호출 전 후로 한 메서드에서 처리했지만
               인터셉터의 경우 다음 메서드로 구분되어있기때문 각 메서드 호출 시점이 다르다.

    return
             - true면 다음 인터셉터나 컨트롤러 호출
             - false면 더이상 진행 x

    HandlerMethod hm = (HandlerMethod) handler
             - 매핑된 컨트롤러 메서드 정보를 알 수 있다.

             - handler : 매핑된 컨트롤러 메서드

     

    더보기
    @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);
    
            //@RequestMapping: HandlerMethod
            //정적 리소스: ResourceHttpRequestHandler
            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);
            }
    
        }
    }

     

    인터셉터 등록 및 설정

     

    • 구현한 인터셉터를 사용하기 위해서는 인터셉터 등록을 해줘야 한다.
    • 필터와 비교해보면 인터셉터는 URL 패턴을 더욱 정밀하게 지정할 수 있다.
    • 아래 방법처럼 new 연산자를 통해 인터셉터 객체를 생성해서 등록해 준다.
      (한번 생성된 인터셉트 계속 사용)
    • 인터셉터를 빈 등록하여 WebConfig에 주입하는 방법으로 해줘도 된다.

     

    @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()를 이용하여 인터셉터를 등록할 수 있다.

     

    더보기

    PathPattern 공식 문서

    ? 한 문자 일치
    * 경로(/) 안에서 0개 이상의 문자 일치
    ** 경로 끝까지 0개 이상의 경로(/) 일치
    {spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
    {spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring"
    {spring:[a-z]+} regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
    {*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처
    /pages/t?st.html — matches /pages/test.html, /pages/tXst.html but not /pages/
    toast.html
    /resources/*.png — matches all .png files in the resources directory
    /resources/** — matches all files underneath the /resources/ path, including /resources/image.png and /resources/css/spring.css
    /resources/{*path} — matches all files underneath the /resources/ path and captures their relative path in a variable named "path"; /resources/image.png will match with "path" → "/image.png", and /resources/css/spring.css will match with "path" → "/css/spring.css"
    /resources/{filename:\\w+}.dat will match /resources/spring.dat and assign the value "spring" to the filename variable

    PathPattern (Spring Framework 5.3.15 API)

     

    PathPattern (Spring Framework 5.3.15 API)

    Representation of a parsed path pattern. Includes a chain of path elements for fast matching and accumulates computed state for quick comparison of patterns. PathPattern matches URL paths using the following rules: ? matches one character * matches zero or

    docs.spring.io

     


     

    Interceptor를 이용한 로그인 인증 처리

     

    로그인 인증 인터셉터 구현

     

    @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("미인증 사용자 요청");
                //로그인으로 redirect
                response.sendRedirect("/login?redirectURL=" + requestURI);
                return false;
            }
    
            return true;
        }
    }

     

    인터셉터 등록 및 설정

     

    @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");
        }
    
    }

     

     

     

     

    반응형

    댓글

Designed by Tistory.