Backend/Spring | SpringBoot

[SpringBoot] 필터와 인터셉터 (ArgumentResolver + @Login 활용)

2245 2023. 9. 21. 23:47

목차

     

    서론: 공통 관심 사항

    애플리케이션을 만들면, 로그인한 사용자만 서비스를 이용하도록 접근 제한 요구사항이 있을 수 있습니다. 

    그럴려면 모든 HTTP 요청마다 로그인을 한 사용자인지 검증하는 로직이 들어가야 합니다. 

    로그인한 사용자에게만 서비스에 접근할 수 있는 버튼을 제공함으로써 해결했다고 할 수 있지만, 버튼 없이도 직접 URL 창에 다음과 같은 URL을 쳐서 해당 서비스를 이용할 수 있습니다. 

     

    http://localhost:8080/items (상품 목록 조회 URL)

     

    구현 방법으로 상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성할 수 있지만, 똑같은 작업을 반복해야 합니다. 

    더 큰 문제는 향후 로그인과 관련된 로직이 변경된다면 작성한 모든 로직을 하나하나 수정해야 합니다.

     

    이렇게 애플리케이션 여러 로직에서 공통으로 관심있어 하는 것을 공통 관심사(cross-cutting-concern)이라고 합니다. 현재는 여러 로직에서 공통으로 인증에 관한 관심을 가지고 있습니다.

    AOP(Aspect-Oriented Programming)

    • AOP는 관점 지향 프로그래밍으로, 프로그램의 핵심 기능과 이에 부가되는 부가 기능(측면, Aspect)을 분리하여 모듈화하는 프로그래밍입니다. 
    • 주요 목적은 코드의 공통 관심사를 분리하여 코드의 모듈성과 재사용성을 증가시키는 것입니다. 공통 관심사란 여러 부분에 걸쳐서 나타나는 로깅, 트랜잭션 관리, 보안 등과 같은 부가 기능을 의미합니다. 
    • 언어나 프레임워크에 따라 구현 방식이 다를 수 있으며, 주로 에스펙트(Aspect)라 불리는 모듈로 공통 관심사를 분리합니다. 

    AOP vs 서블릿 필터/스프링 인터셉터

    • 이러한 공통 관심사는 스프링의 AOP로 해결할 수 있지만, 특히 웹의 요청과 관련된 공통 관심사는 서블릿의 필터 또는 스프링의 인터셉터를 사용하는 것이 더 좋습니다.
    • 왜냐하면 웹과 관련된 공통 관심사를 처리할 때 필요한 HTTP 헤더나 URL의 정보들을 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 통해 제공하기 때문입니다.

    필터 VS 인터셉터 → 인터셉터!

    • 서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심 사항을 처리하므로, 수문장(문지기)를 한다는 측면에서 역할이 같습니다.
    • 다만, 적용되는 순서와 범위, 사용방법이 다릅니다.
    • 스프링 인터셉터가 필터보다 훨씬 더 많은 기능을 제공하고 편리하므로 더 권장합니다.

     

     

    서블릿 필터

    필터는 서블릿이 제공하는 수문장(문지기)입니다.

     

    필터 흐름

    HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
    • 필터가 호출 된 다음 서블릿이 호출됩니다.
    • 그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 됩니다. 
    • 참고로 스프링을 사용하는 경우에 여기서 말하는 서블릿은 디스패처 서블릿으로 생각하면 됩니다.

     

    필터 제한

    HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러 //로그인 사용자
    HTTP 요청 → WAS → 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비로그인 사용자
    • 필터에서 적절하지 않은 사용자라 판단이 되면 서블릿 전에 끝낼 수 있습니다.
    • 따라서 로그인 여부를 체크하기에 좋습니다.

     

    필터 체인

    HTTP 요청 → WAS → 필터1 → 필터2 → 필터3 → 서블릿 → 컨트롤러
    • 필터는 체인으로 구성이 됩니다. 중간에 필터를 자유롭게 추가할 수 있습니다.
    • 예를 들어, 로그를 남기는 필터를 먼저 적용한 후에 로그인 여부를 체크하는 필터를 만들 수 있습니다.

     

    필터 인터페이스

    해당 필터 인터페이스를 구현하여 빈으로 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리합니다. 

    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() {}
    }
    • init() : 필터 초기화 메서드. 서블릿 컨테이너가 생성될 때 호출됩니다.
    • doFilter() : 고객의 요청이 올 때마다 해당 메서드가 호출됩니다. 필터의 로직을 구현하면 됩니다.
    • destory() : 필터 종료 메서드. 서블릿 컨테이너가 종료될 때 호출됩니다.

     

    필터 구현 1. 요청 로그 필터

    가장 단순한 필터인 모든 요청을 로그로 남기는 필터를 개발하고 적용해 봅시다.

    LogFilter - 로그 필터

    @Slf4j
    public class LogFilter implements Filter {	//필터를 사용하려면, Filter 인터페이스를 구현해야 한다. 
    
        /**
         * 필터 초기화
         */
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            log.info("log filter init");
        }
    
        /**
         * 필터 로직 -> HTTP 요청이 오면 해당 메서드 (doFilter)가 호출
         * ServletRequest request는 HTTP 요청이 아닌 경우까지 고려하여 만든 인터페이스
         * HTTP를 사용한다면, HttpServletRequest로 다운캐스팅하여 사용하면 된다.
         */
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest httpRequest = (HttpServletRequest) request;	//다운 캐스팅
            String requestURI = httpRequest.getRequestURI();
    
        	//HTTP 요청을 구분하기 위해, 요청 당 임의의 uuid를 생성 
            String uuid = UUID.randomUUID().toString();
    
        	//Request와 Response 관련 로그 출력
            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 destory");
        }
    }

     

    chain.doFilter(request, response);
    • 가장 중요한 부분입니다.
    • 다음 필터가 있으면, 다음 필터를 호출하고 필터가 없으면 서블릿을 호출합니다.
    • 만약, 이 로직을 호출하지 않으면 다음 단계로 진행되지 않고 애플리케이션이 멈춥니다.

     

    WebConfig - 필터 등록

    필터를 등록하는 방법은 여러 가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용해서 등록하면 됩니다. 

    import javax.servlet.Filter;
    
    @Configuration
    public class WebConfig {
        @Bean
        public FilterRegistrationBean logFilter() {
            FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
            filterRegistrationBean.setFilter(new LogFilter());	//등록할 필터 지정
            filterRegistrationBean.setOrder(1);			//필터는 체인으로 동작하므로 순서 지정 (낮을 수록 먼저 동작)
            filterRegistrationBean.addUrlPatterns("/*");		//필터를 적용할 URL 패턴 지정. 한 번에 여러 패턴 적용 가능 
            return filterRegistrationBean;
        }
    }
    참고 URL 패턴
    URL 패턴에 관한 룰은 필터와 서블릿이 동일합니다. 자세한 내용은 서블릿 URL 패턴을 검색해 봅시다. 
    참고 @WebFilter
    @ServletComponentScan, @WebFilter(filterName = "logFilter", urlPatterns = "/*") 로 필터 등록이 가능하지만, 필터 순서 조절이 안 됩니다. 따라서 FilterRegistrationBean을 사용합시다.

     

    실행 로그

    hello.login.web.filter.LogFilter         : log filter init
    	...
    hello.login.web.filter.LogFilter         : REQUEST [7840f226-f0be-4c4b-ab63-9baf5d660e10][/]
    hello.login.web.filter.LogFilter         : RESPONSE [7840f226-f0be-4c4b-ab63-9baf5d660e10][/]
    hello.login.web.filter.LogFilter         : REQUEST [363afafd-7056-438e-94b2-24ad7f5e7af5][/login]
    hello.login.web.filter.LogFilter         : RESPONSE [363afafd-7056-438e-94b2-24ad7f5e7af5][/login]

     

    참고 logback mdc
    실무에서 HTTP 요청 시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법이 필요할 수 있습니다.
    한 요청이 애플리케이션에 들어와 나갈 때까지 어떤 과정을 거쳤는지 같은 식별자의 로그를 남겨 확인할 수 있습니다. logback mdc로 검색해 봅시다.

     

     

    필터 구현 2. 인증 체크 필터

    로그인을 하지 않은 사용자는 상품 관리 뿐만 아니라 미래에 개발될 페이지에도 접근하지 못하도록 개발해 봅시다.

    LoginCheckFilter -  인증 체크 필터

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

    sendRedirect + requestURI

    httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
    • 만약, 로그인을 하지 않고 상품 목록 서비스에 접근했다가 사용자가 필터에 걸려 로그인 화면으로 리다이렉트되었다고 가정해 봅시다.
    • 그렇다면 로그인을 정상적으로 성공했다면, 원래 로그인 전에 접근했던 상품 목록 서비스로 돌려주는 것이 좋습니다.
    • 이러한 기능을 위해, 현재 요청한 requestURI를 /login 쿼리 파라미터로 함께 전달하여, login 성공 시 해당 requestURI로 이동하도록 구현합니다.

     

    WebConfig - loginCheckFilter() 추가

    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LoginCheckFilter());
        filterRegistrationBean.setOrder(2);     		//logFilter 다음 순서
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

     

    참고 @Autowired
    등록 시 new LoginCheckFilter() 대신, @Component를 사용해 @Autowired를 사용할 수 있습니다. (인터셉터도 마찬가지)
    또한, 필터 내에서 주입받을 객체가 있다면 @Autowired를 통해 주입받을 수 있습니다.
    @Slf4j
    @Component	//컴포넌트 등록
    public class LogFilter implements Filter {​
    @Autowired LogFilter logFilter;
    
    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(logFilter);	//@Autowired 주입
        filterRegistrationBean.setOrder(2);  
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }​​

     

    Login - redirectURL 처리

    LoginController - login()

    /**
     * 로그인 이후 redirect 처리
     */
    @PostMapping("/login")
    public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, @RequestParam(defaultValue = "/") String redirectURL, HttpServletRequest request) {
        if(bindingResult.hasErrors()) {
            return "login/loginForm";
        }
    
        Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
        log.info("login? {}", loginMember);
    
        if(loginMember == null) {
            bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }
    
        //로그인 성공 처리
    
        //세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성
        HttpSession session = request.getSession();
        //세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
        session.setMaxInactiveInterval(30); //1800초
    
        //redirectURL 적용
        return "redirect:" + redirectURL;
    }

     

    실행

    http://localhost:8080/items

     

    로그인하지 않고 /items 접근, 필터로 인해 로그인 폼으로 이동

     

     

    로그인 후 redirectURL로 이동

     

     

    정리: 공통 관심사 분리, 단일 책임 원칙(ISP)

    서블릿 필터를 사용해 공통 관심사를 해결한 덕분에 향후 로그인 관련 정책이 변경되어도, 필터만 수정하면 됩니다. 

    → 단일 책임 원칙, 공통 관심사 분리

    참고 서블릿 필터만의 기능
    스프링 인터셉터는 제공하지 않는 서블릿 필터만이 제공하는 강력한 기능이 하나 있습니다.
    chain.doFilter(request, response);
    위와 같이 다음 필터 또는 서블릿을 호출 할 때, request와 response 대신 ServletRequest 나 ServletResponse 같은 다른 객체로 바꿔서 넘길 수 있습니다. 잘 사용하는 기능은 아니니 참고만 해둡시다.

     

     

     

    스프링 인터셉터 소개

    • 스프링 인터셉터도 서블릿 필터와 동일하게 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술입니다.
    • 필터는 서블릿이 제공하는 기능이고, 인터셉터는 스프링 MVC가 제공하는 기술입니다.
    • 둘 다 수문장의 역할을 하는 것은 동일하지만, 적용되는 순서와 범위, 사용방법이 다릅니다.
    • 스프링 인터셉터가 필터보다 훨씬 더 많은 기능을 제공하고 편리하므로 더 권장합니다.

     

    스프링 인터셉터 흐름

    HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
    • 인터셉터는 디스패처 서블릿과 컨트롤러 호출 직전에 호출됩니다.
    • 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에, 디스패처 서블릿 이후에 등장하게 됩니다.
    • 스프링 MVC의 시작점이 디스패처 서블릿이라고 생각하면 이해가 될 것입니다.
    • 스프링 인터셉터에서 적용하는 URL 패턴은 서블릿 URL 패턴보다 더 편리하고 정밀하게 설정할 수 있다는 장점이 있습니다. 

     

    스프링 인터셉터 제한

    HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러 //로그인 사용자
    HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비로그인 사용자
    • 필터와 동일하게 적절하지 않은 요청이라고 판단되면 더 이상 진행하지 않고 끝냅니다.
    • 그래서 로그인 여부를 체크하기 좋습니다.

     

    스프링 인터셉터 체인

    HTTP 요청 → WAS -> 필터 → 서블릿 → 인터셉터1 → 인터셉터2 → 컨트롤러
    • 서블릿 필터와 마찬가지로 스프링 인터셉터는 체인으로 구성되는데, 중간에 인터셉터를 자유롭게 추가할 수 있습니다.
    • 예를 들어, 로그를 남기는 인터셉터를 먼저 적용하고, 그 다음 로그인 여부를 체크하는 인터셉터를 만들 수 있습니다. 

     

    필터와의 차이점

    • 지금까지의 내용은 서블릿 필터와 호출되는 순서만 다르고, 제공하는 기능은 비슷해보입니다.
    • 앞으로 설명할 내용에서 스프링 인터셉터는 서블릿 필터보다 편리하며, 더 정교하고 다양한 기능을 제공합니다.

     

    스프링 인터셉터 인터페이스

    스프링 인터페이스를 사용하기 위해선 HandlerInterceptor 인터페이스를 구현하여 빈으로 등록하면 됩니다.

    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 {}
    }
    • preHandle() : 컨트롤러 호출 전에 호출됩니다. 필터의 경우, 단순히 request, response만 제공한 것과 달리 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 세세한 컨트롤러 정보도 받을 수 있습니다. 
    • postHandler() : 컨트롤러 호출 후에 호출됩니다. 어떤 ModelAndView가 반환되는지 받을 수 있습니다. 
    • afterCompletion() : 뷰 반환까지 완료된 후에 호출됩니다.

     

    스프링 인터셉터 호출 흐름

    정상 흐름

    • preHandle : 컨트롤러 호출 전에 호출됩니다. (더 정확히는 핸들러 어댑터 호출 전에 호출됩니다.)
      • preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않습니다. false인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않습니다. 그림의 1번에서 끝납니다. 
    • postHandle : 컨트롤러 호출 후에 호출됩니다. (더 정확히는 핸들러 어댑터 호출 후에 호출됩니다.)
    • afterCompletion : 뷰가 렌더링 된 이후에 호출됩니다

     

    스프링 인터셉터 예외 상황

    예외가 발생 시

    • preHandle : 컨트롤러 호출 전에 호출됩니다. 
    • postHandle : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않습니다. 
    • afterCompletion : afterCompletion 은 항상 호출됩니다. 따라서 예외와 무관하게 공통 처리를 하려면 postHandle이 아닌 afterCompletion을 사용해야 합니다. 이 경우 예외( ex )를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있습니다. 

     

    정리

    • 인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 됩니다.
    • 스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리합니다.

     

     

    인터셉터 구현 1. 요청 로그 인터셉터

    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();
    		
            //요청 로그를 구분하기 위해 uuid 생성
            String uuid = UUID.randomUUID().toString();  
            request.setAttribute(LOG_ID, uuid);    //postHandle에서 출력하기 위해 저장 (인터셉터도 싱글톤처럼 관리되기 때문에 멤버 변수 사용 X)
    
            //@RequestMapping => HandlerMethod
            //정적 리소스 => ResourceHttpRequestHandler
            if(handler instanceof HandlerMethod) {
                HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
            }
            
            log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
            return true;    //false는 다음으로 진행 X
        }
    
        @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);	//종료 로그 (예외가 발생해도 호출)
            if(ex!=null) {
                log.error("afterCompletion error!!", ex);
            }
        }
    }
    참고 HandlerMethod
    if(handler instanceof HandlerMethod) {
        HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다. 
    }

    HandlerMethod

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



    ResourceHttpRequestHandler

    @Controller 가 아니라 /resources/static 와 같은 정적 리소스가 호출 되는 경우, ResourceHttpRequestHandler 가 핸들러 정보로 넘어오기 때문에 타입에 따른 처리가 필요합니다. 

     

    WebConfig - 인터셉터 등록

    URL 패턴을 등록하는 부분에서 필터와 비교해서 인터셉터는 addPathPattern, excludePathPatterns를 사용하여 편리하고, 정밀하게 URL 패턴을 지정할 수 있습니다.

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
    	...
    
    	@Override
    	public void addInterceptors(InterceptorRegistry registry) {
    	    registry.addInterceptor(new LogInterceptor())			//인터셉터 등록
    	            .order(1)							//인터셉터 호출 순서 지정 (낮을 수록 먼저)
    	            .addPathPatterns("/**")					//인터셉터를 적용할 URL
    	            .excludePathPatterns("/css/**", "/*.ico", "/error");	//인터셉터를 제외할 패턴
    	}
    
    }

    실행 로그

    REQUEST [2b5252a4-0e0b-4c88-aaf5-4dbff0d7e81c][/][hello.login.web.HomeController#homeLoginV3Spring(Member, Model)]
    postHandle [ModelAndView [view="home"; model={}]]
    RESPONSE [2b5252a4-0e0b-4c88-aaf5-4dbff0d7e81c][/]
    
    REQUEST [b56fdf0e-59aa-4b48-81dc-e7985a631b34][/login][hello.login.web.login.LoginController#loginForm(LoginForm)]
    postHandle [ModelAndView [view="login/loginForm"; model={loginForm=LoginForm(loginId=null, password=null), org.springframework.validation.BindingResult.loginForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}]]
    RESPONSE [b56fdf0e-59aa-4b48-81dc-e7985a631b34][/login]

     

    참고 스프링의 URL 경로
    스프링이 제공하는 URL 경로는 서블릿이 제공하는 URL 경로와 완전히 다릅니다. 더욱 자세하고 세밀하게 지정할 수 있습니다.

    PathPattern 공식 문서 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/
    springframework/web/util/pattern/PathPattern.html

     

     

    인터셉터 구현 2. 인증 체크 인터셉터

    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(false);
            
            if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
                log.info("미인증 사용자 오류");
                //로그인 폼으로 redirect
                response.sendRedirect("/login?redirectURL=" + requestURI);
                return false;
            }
            return true;
        }
    }

    WebConfig - 인터셉터 추가

    @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");	//화이트 리스트를 만들고, 체크하는 로직을 짰던 필터와 달리 매우 편리하다.
    }

    실행 로그: 로그인하지 않은 채 /items(상품 목록) 접근

    REQUEST [d7f21bce-b3ac-4fdc-b38c-d8f352f89644][/][hello.login.web.HomeController#homeLoginV3Spring(Member, Model)]
    postHandle [ModelAndView [view="home"; model={}]]
    RESPONSE [d7f21bce-b3ac-4fdc-b38c-d8f352f89644][/]
    
    REQUEST [df7cce76-ed9a-4512-ae1a-76dbd6840fef][/items][hello.login.web.item.ItemController#items(Model)]
    인증 체크 인터셉터 실행 /items
    미인증 사용자 오류
    RESPONSE [df7cce76-ed9a-4512-ae1a-76dbd6840fef][/items]
    REQUEST [76660ea3-f597-4bce-ba4e-1e43e18161c2][/login][hello.login.web.login.LoginController#loginForm(LoginForm)]
    postHandle [ModelAndView [view="login/loginForm"; model={loginForm=LoginForm(loginId=null, password=null), org.springframework.validation.BindingResult.loginForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}]]
    RESPONSE [76660ea3-f597-4bce-ba4e-1e43e18161c2][/login]

     

     

     

    ArgumentResolver, @Login 활용

    • 세션을 사용하여 로그인 회원을 정보를 찾아올 때, ArgumentResolver과 @Login 애노테이션을 직접 만들어 적용함으로써 공통 관심사로 분리하여 편리하게 찾아올 수 있습니다.
    • @Login 애노테이션이 붙은 메서드가 있다면, 직접 만든 ArgumentResolver가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아 전달하고, 없다면 null을 전달하도록 개발해 봅시다. 

    HomeController 수정

    @SessionAttribute 대신 @Login 애노테이션으로 변경합니다.

    @GetMapping("/")
    public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
    
        //세션에 회원 데이터가 없으면 home
        if(loginMember == null) {
            return "home";
        }
    
        //세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }

     

    수정 전

    @GetMapping("/")
    public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember , Model model) {
        ...
    }

     

    @Login 애노테이션 생성

    @Target(ElementType.PARAMETER)		//파라미터에만 적용
    @Retention(RetentionPolicy.RUNTIME)	// 리플렉션 등을 활용할 수 있도록, 런타임까지 애노테이션 정보가 남아 있음 
    public @interface Login {
    }

     

    LoginMemberArgumentResolver 생성

    @Slf4j
    public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
    
        /**
         * @Login 애노테이션이 있고, Member 타입이면 해당 ArugumentResolver가 사용됩니다. 
         */
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            log.info("supportsParameter 실행");
            boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);	
            boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
    
            return hasLoginAnnotation && hasMemberType;
        }
    
        /**
         * 컨트롤러 호출 직전에 호출되어 필요한 파라미터 정보를 생성하여 전달해줍니다.
         * 해당 메서드에선 세션이 있는 로그인 회원 정보인 member 객체를 찾아 반환해줍니다.
         */
        @Override
        public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
            log.info("resolveArgument 실행");
    
            HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
            HttpSession session = request.getSession(false);
            if(session == null) {
                return null;
            }
    
            return session.getAttribute(SessionConst.LOGIN_MEMBER);
        }
    }

     

    WebMVCConfigurer에 설정 추가

    앞서 개발한 LoginMemberArgumentResolver를 등록해줍니다.

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }

     

     


    출처

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

     

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

    웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

    www.inflearn.com