ABOUT ME

-

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

     

     

     

     

     

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

     

     

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

     

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

     

     특정 페이지의 조건을 체크하는 공통의 관심사(Cross-Cutting Concern) 해결해기 위해서 서블릿 필터 또는 스프링 인터셉터를 사용하여 처리하고, 필터 및 인터셉터는 HttpServletRequest가 제공하는 HTTP의 헤더와 URL의 정보들을 이용하여 공통의 관심사를 처리해준다.

     

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

     

     

     

     

    Filter

    서블릿이 제공하는 기술

     

    필터 흐름

     

    HTTP 요청  →  WAS  →  필터  →  디스패처 서블릿  → 컨트롤러

     

    필터를 이용한 요청 제한

     

    HTTP  →  WAS  →  필터  →  디스패처 서블릿  →  컨트롤러     ex) 로그인 사용자
    HTTP  →  WAS  →  필터 (조건에 부합하지 않음, 서블릿 호출 X)   ex) 비로그인 사용자​

     

     

     필터에서 해당 조건에 부합하지 않은 요청이라 판단하면 컨트롤러에 접근하기 위한 디스패처 서블릿을 호출하지 않는다. 


     

    필터 체인

     

    HTTP 요청  →  WAS  → 필터1  →  필터2  → 필터3  →  디스패처 서블릿  →  컨트롤러

     

     필터는 체인으로 구성되는데 중간에 필터를 자유롭게 추가할 수 있어 여러 조건을 체크할 수 있다.


     

    필터 인터페이스

     

    • Filter 인터페이스는 다음과 같이 3가지 메서드를 제공한다.
    • init()       : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
    • doFilter()  : 고객의 요청이 올 때마다 해당 메서드가 호출, 필터의 로직(조건 체크)을 구현하는 곳
    • destroy()  : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출

     

    public interface Filter {
    
        public default void init(FilterConfig filterConfig) throws ServletException {}
        public void doFilter(ServletRequest request, ServletResponse response,
                FilterChain chain) throws IOException, ServletException;
        public default void destroy() {}
    }

     

     

    Filter를 이용한 요청 로그 기록

     

    로그 필터 구현

     

    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 httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();
    
        String uuid = UUID.randomUUID().toString();
    
        try {
            log.info("REQUEST [{}][{}]", uuid, requestURI);
            chain.doFilter(request, response);
        } catch (Exception e) {
            throw e;
        } finally {
            log.info("RESPONSE [{}][{}]", uuid, requestURI);
        }
    
    }

    ▶ HTTP 요청이 올 때마다 doFilter 메서드가 호출된다.

    HttpServletRequest httpRequest = (HttpServletRequest) request

           - ServletRequest request는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스

           - HTTP 헤더와 URL 정보를 이용하기 위해 다운 캐스팅을 해준다.

    String uuid = UUID.randomUUID().toString()
           - HTTP 요청을 구분하기 위한 로그 기록을 위해 요청당 임의의 uuid 생성

    log.info("REQUEST [{}][{}]", uuid, requestURI)

           - 요청에 따른 uuid와 요청 URL 로그 출력

    chain.doFilter(request, response)
           - 다음 필터가 있으면 필터를 호출하고 없으면 서블릿을 호출한다.
           - 이 로직이 없으면 다음 단계로 진행되지 않는다.

     

     

    더보기
    package hello.login.web.filter;
    
    import lombok.extern.slf4j.Slf4j;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.util.UUID;
    
    @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 httpRequest = (HttpServletRequest) request;
            String requestURI = httpRequest.getRequestURI();
    
            String uuid = UUID.randomUUID().toString();
    
            try {
                log.info("REQUEST [{}][{}]", uuid, requestURI);
                chain.doFilter(request, response);
            } catch (Exception e) {
                throw e;
            } finally {
                log.info("RESPONSE [{}][{}]", uuid, requestURI);
            }
    
        }
    
        @Override
        public void destroy() {
            log.info("log filter destroy");
        }
    }
    

     


     

     

    필터 등록 및 설정

     

    • 구현한 필터를 사용하기 위해서는 컨테이너에 빈 등록을 해야 한다.

     

    @Configuration
    public class WebConfig {
    
        @Bean
        public FilterRegistrationBean logFilter() {
            FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
            filterRegistrationBean.setFilter(new LogFilter());
            filterRegistrationBean.setOrder(1);
            filterRegistrationBean.addUrlPatterns("/*");
    
            return filterRegistrationBean;
        }
    
    }

    FilterRegistrationBean
           - 스프링 부트 사용 시 필터 등록을 위한 클래스

    ▶ setFilter(new LogFilter())

           - 등록할 필터 지정

    setOrder(1)

           - 필터의 순서를 지정(높을수록 우선순위가 높다)

    addUrlPatterns("/*")

           - 필터에 적용할 URL 패턴을 지정

           - 한번에 여러 패턴 지정 가능( addUrlPatterns("/url1", "/url2") )

     

     

     

    참고
    @ServletComponentScan,
    @WebFilter(filterName = "필터구현체", urlPatterns = "URL")
    을 필터 구현체에 붙여서 사용해도 되지만 필터 순서를 지정할 수 없다.

     

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

     

     

    로그인 인증 필터 구현

     

    @Slf4j
    public class LoginCheckFilter implements Filter {
    
        private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String requestURI = httpRequest.getRequestURI();
    
            HttpServletResponse httpResponse = (HttpServletResponse) response;
    
            try {
                log.info("인증 체크 필터 시작 {}", requestURI);
    
                if (isLoginCheckPath(requestURI)) {
                    log.info("인증 체크 로직 실행 {}", requestURI);
                    HttpSession session = httpRequest.getSession(false);
                    if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
    
                        log.info("미인증 사용자 요청 {}", requestURI);
                        //로그인으로 redirect
                        httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
                        return;
                    }
                }
    
                chain.doFilter(request, response);
            } catch (Exception e) {
                throw e; //예외 로깅 가능 하지만, 톰캣까지 예외를 보내주어야 함
            } finally {
                log.info("인증 체크 필터 종료 {} ", requestURI);
            }
    
        }
    
        /**
         * 화이트 리스트의 경우 인증 체크X
         */
        private boolean isLoginCheckPath(String requestURI) {
            return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
        }
    }

    private static final String[] whitelist

            - 필터를 적용 제외 URL 모음

    if (isLoginCheckPath(requestURI)) {

            - 화이트 리스트를 제외한 모든 경우에 로그인 인증 처리

    ▶ httpResponse.sendRedirect("/login?redirectURL=" + requestURI)

            - 비로그인 사용자를 로그인 화면으로 리다이렉트 처리

            - 로그인 화면을 처리하는 컨트롤러에서 해당 URL을 받아 로그인 시 해당 URL로 이동 처리
              해주기 위해 requestURL을 쿼리 파라미터로 전달

    return

            - 필터를 더이상 진행시키지 않고 반환

            - redirect를 이용하여 새로운 요청(/login?redirectURL=requestURI)으로 해당 필터 적용

    ▶ chain.doFilter(request, response)
           - 다음 필터가 있으면 필터를 호출하고 없으면 서블릿을 호출한다.
           - 이 로직이 없으면 다음 단계로 진행되지 않는다.


     

    필터 등록 및 설정

     

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Bean
        public FilterRegistrationBean loginCheckFilter() {
            FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
            filterRegistrationBean.setFilter(new LoginCheckFilter());
            filterRegistrationBean.setOrder(2);
            filterRegistrationBean.addUrlPatterns("/*");
    
            return filterRegistrationBean;
        }
    }

     FilterRegistrationBean
           - 스프링 부트 사용 시 필터 등록을 위한 클래스

    ▶ setFilter(new LogFilter())

           - 등록할 필터 지정

     setOrder(2)

           - 필터의 순서를 지정(높을수록 우선순위가 높다)

     addUrlPatterns("/*")

           - 필터에 적용할 URL 패턴을 지정

           - 한번에 여러 패턴 지정 가능( addUrlPatterns("/url1", "/url2") )


     

     

     위 두개의 필터를 적용시켰을때 흐름은 다음과 같다.

     

    1. URL 요청 : localhost:8080/item 
         - item 페이지는 로그인 이후에 이용할 수 있는 페이지
    2. LogFilter의 doFilter 메서드 실행
         - 필터의 우선 순위가 가장 높은 LogFilter가 먼저 호출
         - log.info("REQUEST [{}][{}]", uuid, requestURI) 로그 출력
         - chain.doFilter(request, responsse) 로 다음 필터 호출
    3. LoginCheckFilter의 doFilter 메서드 실행
         - 다음 우선 순위가 높은 LoginCheckFilter 호출
         - 로그인 세션이 있는지 확인후 없으면 /login으로 redirect
    4. redirect로 인한 새로운 URL 요청 : localhost:8080/login?redirectURL=requestURI
    5. LogFilter의 doFilter 메서드 실행
         - 필터의 우선 순위가 가장 높은 LogFilter가 먼저 호출
         - log.info("REQUEST [{}][{}]", uuid, requestURI) 로그 출력
         - chain.doFilter(request, responsse) 로 다음 필터 호출
    6. LoginCheckFilter의 doFilter 메서드 실행
         - 다음 우선 순위가 높은 LoginCheckFilter 호출
         - 해당 필터 제외 URL인 /login이므로 로그인 확인 체크 제외
         - chain.doFilter(request, responsse) 로 URL요청에 매핑된 컨트롤러 호출
    7. 로그인 성공시 처음 요청했던 URL로 이동

     

     

     

     

     

     

     

     

     

     

     

    반응형

    댓글

Designed by Tistory.