관리 메뉴

나구리의 개발공부기록

로그인 처리2 - 필터/인터셉터, 서블릿 필터(소개/요청로그/인증체크), 스프링 인터셉터(소개/요청 로그/ 인증체크), ArgumentResolver 활용 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술

로그인 처리2 - 필터/인터셉터, 서블릿 필터(소개/요청로그/인증체크), 스프링 인터셉터(소개/요청 로그/ 인증체크), ArgumentResolver 활용

소소한나구리 2024. 9. 6. 13:51

  출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/GMo43


1. 서블릿 필터 - 소개

1) 공통 관심 사항

  • 로그인 한 사용자만 상품 관리 페이지에 들어가야 하는데 지금은 로그인 하지 않은 사용자도 URL을 직접 호출하게 되면 상품 관리 화면에 들어갈 수 있음
  • 상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 적용해주면 되지만 이런 작은 프로젝트에도 등록, 수정, 삭제, 조회 등등 모든 컨트롤러 적용을 하기란 쉽지않고, 규모가 큰 경우에는 더욱 어려움
  • 향후 로그인과 관련된 로직이 변경 될 때마다 작성한 관련 모든 로직을 다 수정해야하는 더 큰 문제도 발생됨
  • 이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 것을 공통 관심사(cross-cutting concern)라고 함
  • 이런 공통 관심사는 스프링의 AOP로도 해결할 수 있지만 웹과 관련된 공통 관심사는 부가적인 다양한 기능을 제공하는 서블릿 필터혹은 스프링 인터셉터를 사용하는 것이 좋음
  • 웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데 서블릿필터, 스프링 인터셉터 모두 HttpServletRequest를 제공함

2) 서블릿 필터의 특성

(1) 필터 흐름

  • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
  • 필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출 됨
  • 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 됨
  • 필터는 특정 URL 패턴에 적용할 수 있으며 /* 이라고 하면 모든 요청에 필터가 적용됨
  • 여기서 말하는 서블릿은 스프링의 디스패처 서블릿임

(2) 필터 제한

  • 로그인 사용자: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
  • 비 로그인 사용자 : HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출 X)
  • 필터에서 적절하지 않은 요청이라고 판단하면 거기서 끝낼 수 있기 때문에 로그인 여부를 체크하기에 좋음

(3) 필터 체인

  • HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
  • 필터는 체인으로 구성되어 중간에 필터를 자유롭게 추가할 수 있음
  • ex) 로그를 남기는 필터 적용 -> 로그인 여부를 체크하는 필터를 적용 등

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

2. 서블릿 필터 - 요청 로그

1) 모든 요청을 다남기는 필터 개발 - LogFilter

  • Filter를 implements로 구현
  • HTTP 요청이 오면 doFilter가 호출됨
  • ServletRequest는 HTTP요청이 아닌 경우까지 고려해서 만든 인터페이스이므로 HTTP를 사용하면 HttpServletRequest로 다운 캐스팅 적용해서 사용
  • chain.doFilter(request, response): 다음 필터가 있으면 필터를 호출하고 필터가 없으면 서블릿을 호출하는 코드인데, 이부분을 빠트리면 다음 단계로 진행되지 않음(먹통이 됨)
package hello.login.web.filter;

@Slf4j
public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("LogFilter init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest,
            ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
            
        log.info("LogFilter doFilter");

        // ServletRequest를 기능이 많은 HttpServletRequest로 다운 캐스팅
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String requestURI = httpServletRequest.getRequestURI();   // URI 정보 가져오기
        String uuid = UUID.randomUUID().toString();               // HTTP 요청을 구분하기 위한 UUID

        try {
            log.info("REQUEST [{}][{}]", uuid, requestURI);
            filterChain.doFilter(servletRequest,  servletResponse);     // 다음 필터 호출, 없으면 서블릿 호출
        } catch (Exception e) {
            throw e;
        } finally {
            log.info("RESPONSE [{}][{}]", uuid, requestURI);
        }
    }

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

2) Webconfig - 필터 설정

  • 필터를 등록하는 방법이 여러가지 있지만 스프링 부트를 사용한다면 FilterRegistrationBean을 사용해서 등록하면 됨
  • setFilter(new LogFilter()): 등록할 필터를 지정
  • setOrder(1): 우선순위 지정, 필터는 체인으로 동작하므로 순서 지정이 필요함, 낮을 수록 먼저 동작
  • addUrlPatterns("/*"): 필터를 적용할 URL 패턴을 지정 , 한번에 여러 패턴을 지정할 수 있음
package hello.login;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LogFilter());  // 필터 적용
        filterFilterRegistrationBean.setOrder(1);                 // 순서
        filterFilterRegistrationBean.addUrlPatterns("/*");        // 모든 URL에 적용

        return filterFilterRegistrationBean;
    }

}

실행 로그 내역

 

** 참고

  • URL 패턴에 대한 룰은 필터도 서블릿과 동일하니 서블릿 URL 패턴으로 검색해서 확인
  • @ServletComponentScan @WebFilter(filterName = "logFilter", urlPatterns = "/*")로 필터 등록이 가능하지만 순서 조절이 안되므로 FilterRegistrationBean을 사용
  • 실무에서 HTTP 요청 시 같은 요청의 로그에 모든 같은 식별자를 자동으로 남기는 방법은 logback mdc를 검색해서 적용하면 됨

3. 서블릿 필터 - 인증 체크

  • 로그인 되지 않은 사용자는 상품 관리 뿐만 아니라 개발될 페이지에도 접근하지 못하도록 인증 체크 필터 개발

1) LoginCheckFilter - 인증 체크 필터

(1) whitelist

  • WebConfig에 필터를 /*로 모든경로 적용하고 whitelist를 작성하여 로그인하지 않아도 접근할 수 있는 경로를 작성
  • 이렇게 로직을 적용하면 추가적으로 개발되는 페이지도 모두 접근할 수 없고 whitelist에 추가해야지만 접근이 허용됨

(2) isLoginCheckPath메서드

  • 작성한 whitelist를 적용하기위한 메서드를 구현해서 적용
  • whitelist와 요청URL이 일치하지 않으면 인증 체크 로직을 실행함

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

  • 미인증 사용자는 로그인화면으로 리다이렉트 -> 로그인 이후 다시 보던 페이지로 복귀할 수 있도록 requestURI를 /login에 쿼리파라미터로 전달
  • 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능을 추가로 개발해야함
  • 이렇게 개발자가 귀찮아도 사용자 입장에서 편의성을 제공하는 개발을 해야 개발을 잘하는 것

(4) return;

  • 이렇게 반환해버리면 필터를 더 진행하지 않고 서블릿, 컨트롤러도 호출되지 않고 앞서 적용한 redirect만 응답으로 적용되고 요청이 끝남
package hello.login.web.filter;

@Slf4j
// 인터페이스에 default라고 붙은 메서드는 전부 구현하지 않아도 됨 - init, destroy는 구현 제외
public class LoginCheckFilter implements Filter {

    // 접근할 수 있는 whitelist 작성
    private static final String[] whitelist = {"/", "members/add", "/login", "/logout", "/css/*"};

    @Override
    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse, FilterChain filterChain)
                            throws IOException, ServletException {

        // 다운캐스팅
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

        String requestURI = httpRequest.getRequestURI();    // URI 받기

        try {
            log.info("인증 체크 필터 시작: {}", requestURI);

            if (isLoginCheckPath(requestURI)) {
                log.info("인증 체크 로직 실행: {}", requestURI);
                HttpSession session = httpRequest.getSession(false);

                // 세션이 null이거나 getAttribute의 값이 null이면 미인증 사용자
                if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
                    log.info("미인증 사용자 요청: {}", requestURI);

                    // 로그인으로 redirect, 로그인을 성공하면 원래의 페이지로 복귀하도록 하기 위한 초석
                    httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
                    return;     // 이렇게 리턴하면 서블릿이나 컨트롤러 호출 안함
                }
            }
            filterChain.doFilter(servletRequest, servletResponse);
        } catch (Exception e) {
            throw e;        // 예외를 로깅가능 하지만 톰캣까지 예외를 보내주어야 함
        } finally {
            log.info("인증 체크 필터 종료: {}", requestURI);
        }

    }

    /*    화이트 리스트의 경우 인증 체크 X     */
    private boolean isLoginCheckPath(String requestURI) {
        // PatternMatchUtils 활용
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
    }

}

2) WebConfig - loginCheckFilter() 추가

  • 기존에 작성했던 설정에 순서를 2로 적용하고 LoginCheckFilter()를 사용하도록 적용
  • addUrlPatterns를 "/*"로 설정하여 모든 요청에 적용
@Bean
public FilterRegistrationBean loginCheckFilter() {
    FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
    filterFilterRegistrationBean.setFilter(new LoginCheckFilter()); // 필터 적용
    filterFilterRegistrationBean.setOrder(2);                       // 순서 적용 - 2
    filterFilterRegistrationBean.addUrlPatterns("/*");              // 모든 URL에 적용

    return filterFilterRegistrationBean;
}

3) RedirectURL 처리를 위한 LoginController - loginV4() 추가

  • 기존의 v3 버전에 파라미터로 @RequestParam을 추가하여 파라미터로 넘겨받은 URL주소로 다시 redirect하도록 컨트롤러 수정
  • 이렇게 적용하면 로그인 시 home으로 돌아가는 것이 아닌 기존의 화면으로 리다이렉트하게 됨
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute("loginForm") LoginForm loginForm, BindingResult bindingResult,
                      @RequestParam(defaultValue = "/") String redirectURL, // 추가
                      HttpServletRequest httpServletRequest) {
 	
    // .. 전부 동일

    return "redirect:" + redirectURL;   // 없으면 /로 이동, 있으면 해당 URL로 이동
}

 

4) 정리

  • 서블릿 필터를 적용하여 로그인을 하지않은 사용자는 나머지 경로에 들어갈 수 없게 되었음
  • 공통 관심사를 서블릿 필터를 사용해서 해결한 덕분에 향후 로그인 정책이 변경되어도 해당 필터만 변경하면됨 (공통 관심사 분리, 단일책임 원칙을 잘 지킴)
  • 스프링 시큐리티(Spring Security)를 포함한 웹 애플리케이션 인증 관련 로직으로 이와같은 개념으로 구현되어있어서 개념을 잘 파악하고 응용하면 됨
  • 서블릿 필터는 chain.doFilter(request, response);를 호출해서 다음 필터 혹은 서블릿을 호출할 때 ServletRequest, ServletResponse를 구현한 다른 객체로 변경할 수 있음 -> 변경된 객체가 다음 필터 또는 서블릿에서 동작됨
    (잘 사용하는 기능은 아니므로 참고만 할 것)

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

  • 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기능
  • 서블릿 필터는 서블릿이, 스프링 인터셉터는 스프링 MVC가 제공하는 기술
  • 스프링 인터셉터는 MVC 구조에 특화된 필터 기능을 제공하기 때문에 웬만해서(꼭 서블릿 필터를 사용해야 하는 상황이 아니라면)는 서블릿 필터보다 스프링 인터셉터를 사용하는 것이 더욱 편리하고 좋음

1) 스프링 인터셉터 특징

(1) 스프링 인터셉터 흐름

  • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스플링 인터셉터 -> 컨트롤러
  • 스프링 인터셉터는 디스패터 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 됨
  • 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 호출됨(스프링 MVC 시작점이 디스패처 서블릿이라고 생각해보면 이해가 됨)
  • 서블릿 URL 패턴과는 다른 매우 정밀하게 설정할 수 있는 URL 패턴을 적용할 수 있음

(2) 스프링 인터셉터 제한

  • 로그인 사용자: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
  • 비로그인 사용자: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X)
  • 필터와 마찬가지로 적절하지 않은 요청이라고 판단하면 중단시킬 수 있기 때문에 로그인 여부를 체크하기 좋음

(3) 스프링 인터셉터 체인

  • HTTP  -> 요청 -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> ... -> 컨트롤러
  • 스프링 인터셉터도 체인으로 구성되며 중간에 인터셉터를 자유롭게 추가할 수 있음
  • 예를 들면 로그를 남기는 인터셉터를 적용하고, 로그인 여부를 체크하는 인터셉터를 적용하는 등으로 사용할 수 있음
  • 서블릿 필터와 호출 되는 순서만 다르고 비슷해 보이지만 서블릿 필터보다 편리하고 더 정교하고 다양한 기능을 지원함

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

  • 스프링 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 됨
  • 서블릿 필터와의 차이는 doFilter() 하나만 제공되며 다음 필터를 호출하는 doFilter()를 빠트리는 실수를 자주 하게 되지만, 인터셉터는 컨트롤러 호출 전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)과 같이 단계적으로 잘 세분화 되어 있음
  • 또한 request, response만 제공했던 서블릿 필터와는 달리 어떤 컨트롤러(handler)가 호출 되는지 호출 정보도 받을 수 있고, 어떤 modelAndView가 반환 되는지 응답 정보도 받을 수 있음
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 {
    }
}

3) 스프링 인터셉터 정상 흐름

(1) pre Handle

  • 컨트롤러 호출 전에 호출 (정확히는 핸들러 어댑터 호출 전에 호출)
  • preHandle의 응답값이 ture이면 다음으로 진행하고 false이면 더는 진행하지 않으며 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않고 1번에서 끝나게 됨

(2) postHandle

  • 컨트롤러 호출 후에 호출 (정확히는 핸들러 어댑터 호출 후에 호출)

(3) afterCompletion

  • 뷰가 렌더링 된 이후에 호출

정상 흐름도

 

4) 스프링 인터셉터 예외 상황

(1) PreHandle

  • 컨트롤러 호출 전에 호출 됨

(2) postHandle 

  • 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않음

(3) afterCompletion

  • 예외가 발생 해도 항상 호출됨,  예외(ex)를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력 할 수 있음
  • 예외가 무관하게 공통 처리를 할 경우 afterCompletion()을 사용해야함

스프링 인터셉터 예외 발생 상황


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

1) LogInterceptor - 요청 로그 인터셉터 개발

(1) request.setAttribute(LOGID, uuid)

  • 서블릿 필터틑 지역변수로 uuid값을 모두 확인할 수 있지만 스프링 인터셉터는 호출 시점이 완전히 분리 되어있어 preHandle에서 지정한 값을 postHandle, afterCompletion에서 함께 사용하려면 어딘가에 담아 두어야 하는데 이렇게 settAttribute를 활용해서 담아두고 꺼낼때는 reqeust.getAttribute()로 찾아서 활용하면 됨
  • LogInterceptor도 싱글톤 처럼 사용되기 때문에 멤버변수로 사용하게 되면 다른곳에서 사용시 바뀌어버리기 때문에 위험함

(2)  return 값

  • true: 정상 호출 -> 다음 인터셉터나 컨트롤러 호출
  • false: 즉시 종료

(3) 핸들러 정보 타입

  • 스프링 사용시 보통 @Controller, @RequestMapping을 활용한 핸들러 매핑을 사용 -> HandlerMethod가 정보로 넘어옴
  • 정적 리소스가 호출 되는 경우 -> ResourceHttpRequestHandler가 정보로 넘어옴

(4) 종료 로그

  • 종료 로그(RESPONSE)를 postHandle이 아닌 afterCompletion에서 설정한 이유는 postHandle은 예외가 발생한 경우 호출 되지 않지만 afterCompletion은 예외가 발생하도 호출 되는 것을 보장하기 때문
package hello.login.web.interceptor;

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOGID = "logid";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        /*  예외가 터지면 바로 종료되므로 afterCompletion 에서 로그를 서로 확인할 수 가 없는데
         setAttribute 로 uuid 를 저장하고, getAttribute 로 조회하면 logid를 받을 수 있음 */
        request.setAttribute(LOGID, uuid);

        // @RequestMapping 사용: HandlerMethod 사용
        // 정적 리소스를 사용: ResourceHttpRequestHandler 사용
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;// 호출할 컨트롤러의 메서드의 모든 정보가 포함되어있음
        }

        log.info("REQUEST  [{}][{}][{}]", uuid, requestURI, handler);   // 로그 찍기
        return true;    // false -> 바로 끝남, true -> 다음 호출
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) throws Exception {

        log.info("postHandle {}", modelAndView);    // modelAndView 로그 찍기
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        String requestURI = request.getRequestURI();
        String uuid = (String)request.getAttribute(LOGID);  // preHandle에서 가져온 uuid

        log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);

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

    }
}

 

2) WebConfig - 인터셉터 등록

(1) WebMvcConfigurer가 제공하는 addInterceptors()를 사용해서 인터셉터를 등록

  • addInterceptor(): 인터셉터 등록
  • order(1): 호출 순서를 지정, 낮을 수록 먼저 호출
  • addPathPatterns("/**"): 인터셉터를 적용할 URL 패턴을 지정
  • excludePathPatterns("/css/**", ... "/error"): 인터셉터에서 제외할 패턴을 지정

(2) 필터와의 비교

  • 인터셉터는 addPathPatterns, excludePathPatterns로 더 정밀하게 URL 패턴을 지정할 수 있음
  • 로그를 확인해보면 더욱 다양한 정보가 담겨있어 해당 값을들 자유롭게 호출해서 사용할 수 있음
  • 스프링이 제공하는 URL 경로는 서블릿 기술이 제공하는 URL경로와 완전히 다르며 더욱 자세하고 세밀하게 설정할 수 있음

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이라는 변수로 캡처

 

공식문서 링크

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

 

@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 인터셉터 설정 -> WebMvcConfigurer 인터페이스를 구현하고 addInterceptors 메서드 구현
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())                       // 등록
                .order(1)                                                   // 순서
                .addPathPatterns("/**")                                     // 전체 URL에 필터 적용
                .excludePathPatterns("/css/**", "/*.ico", "/error");        // 필터를 적용하지 않을 경로
    }
    
    // ...
}

 


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

1) LoginCheckInterceptor

  • 인증은 컨트롤러 호출 전에만 호출되면 되기 때문에 preHandle만 구현하면 됨 
  • preHandle 구현시 whitelist 등록 및 체크와 같은 코드를 Config 설정 때 간단히 할 수 있기 때문에 필터에 비해 코드가 전체적으로 간결해짐
package hello.login.web.interceptor;

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

    // 인증 체크는 preHandle만  구면하면 됨
    // whitelist 등록 및 체크하는 코드들이 전부 사라짐 -> WebConfig에 등록하는걸로 해결할 수 있음
    @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;
    }
}

2) WebConfig에 로그인체크인터셉터 등록

  • 기존 인터 셉터를 구현한 addInterceptors 메서드 아래에 registry.addInterceptor()로 만들어 둔 new LogincheckInterceptor()를 생성하면 등록 됨
  • chain 형태로 순서, 필터 적용 URL, 필터 적용하지 않을 URL을 쭉 등록 해주면됨
  • URL 설정 전략을 매우 세밀하게 적용 가능하고, 여러 인터셉터를 등록하는 방법이 서블릿 필터와 비교해보면 매우 편리해짐
@Configuration
public class WebConfig implements WebMvcConfigurer {

    // 인터셉터 설정 -> WebMvcConfigurer 인터페이스를 구현하고 addInterceptors 메서드 구현
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())                       // 인터셉터 등록
                .order(1)                                                   // 순서
                .addPathPatterns("/**")                                     // 전체 URL에 필터 적용
                .excludePathPatterns("/css/**", "/*.ico", "/error");        // 필터를 적용하지 않을 경로


        registry.addInterceptor(new LoginCheckInterceptor())          // 로그인체크인터쳅터를 등록
                .order(2)                                             // 순서 등록
                .addPathPatterns("/**")                               // 필터 적용 경로
                // 필터를 적용하지 않을 경로
                .excludePathPatterns("/", "/members/add", "/login", "/logout",
                                    "/css/**", "/*.ico", "/error");
    }
}

3) 정리

  • 서블릿 필터, 스프링 인터셉터는 둘다 공통 관심사를 해결하기 위한 기술
  • 실제로 둘다 구현해보면 스프링 인터셉터가 개발자 입장에서는 훨씬 편리하기 때문에 특별한 문제가 없다면 인터셉터를 사용하면 됨

7. ArgumentResolver 활용

  • MVC1편 6. 스프링 MVC - 기본기능 -> 요청 매핑 핸들러 어뎁터 구조에서 ArgumentResolver를 학습했는데, 해당 기능을 사용해서 로그인 회원을 더 편리하게 찾을 수 있음
  • 실행 해보면 결과는 동일하지만 공통 작업이 필요할 때 ArgumentResolver를 활용하면 컨트롤러를 더욱 편리하게 사용할 수 있음
  • 애노테이션, ArgumentResolver를 응용해서 개발하면 공통 작업을 여러 개발자가 사용할 수 있기 때문에 프로젝트 개발이 편리해짐
  • 내부에 캐시 기능이 있어서 supportsParameter()는 한번만 실행되고 이후에는 resolveArgument만 실행됨

1) HomeController 추가 - homeLoginV3ArgumentResolver()

  • @SessionAttribute를 지우고 직접 만들 @Login 애노테이션을 파라미터에 적용
  • @Login이 있으면 직접만든 ArgumentResolver가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고 만약 세션에 없다면 null을 반환하도록 개발
// ArgumentResolver 활용, @Login 애노테이션을 직접 만들고 적용하여 컨트롤러 코드를 간편하게 만듦
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {

	// ... 메서드 동일
}

2) @Login 애노테이션 생성

  • @Target(ElementType.PARAMETER): 파라미터에만 적용
  • @Retention(RetentionPolicy.RUNTIME): 런타임 까지 애노테이션 정보가 남아있음
package hello.login.web.argumentresolver;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}

3) LoginMemberArgumentResolver 생성

  • supportsParameter(): @Login 애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver가 사용됨
  • resolveArgument(): 컨트롤러 호출 직전에 호출되어서 필요한 파라미터 정보를 생성해줌(여기에서는 세션에 있는 로그인 회원정보인 member 객체를 찾아서 반환)
package hello.login.web.argumentresolver;

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        log.info("supportsParameter 실행");

        // @Login 애노테이션이 파라미터에 붙어있어있는지 확인 여부를 반환
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);

        // Member 타입과 파라미터의 타입이 같은지 확인 여부를 반환
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        // 둘의 조건이 모두 true 반환이 되면 resolveArgument() 를 실행
        return hasLoginAnnotation && hasMemberType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
                                    throws Exception {

        log.info("resolveArgument 실행");

        // get.NativeRequest()를 HttpServletRequest 타입으로 변환
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);    // 세션 추가, false: 새로생성 x

        // 세션이 널이면 파라미터 타입에 null 반환
        if (session == null) {
            return null;
        }

        // 있으면 세션정보를 지정한 상수의 이름으로 반환
        return session.getAttribute(SessionConst.LOGIN_MEMBER);
    }
}

4) WebConfig에 addArgumentResolvers 오버라이딩

  • WebMvcConfigurer를 구현한 설정 클래스에 addArgumentResolvers()를 구현하여 작성한 ArgumentResolver를 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {

    // addArgumentResolvers() 구현 -> ArgumentResolver 등록하기 위함
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());   // resolvers.add()에 new 로 작성한 리졸버를 등록
    }
    
    // ...
}

 

** 참고

  • 교안에서는 만든 서블릿 필터, 스프링 인터셉터를 new로 생성하여 적용하였지만 모두 @Component로 스프링빈으로 등록해서 Config에 등록 시 @Autowired로 주입받은뒤 주입받은 변수를 활용해서 등록할 수 있음