[Spring] Filter, Interceptor

dh·2024년 6월 10일
1

인증(Authentication)

목록 보기
2/5
post-thumbnail


로그인하지 않은 사용자가 글을 조회하고 작성하는 것을 막으려면 컨트롤러에서 로그인체크로직을 하나하나 작성하면 되겠지만, 향후 로직이 변경될 경우 일일이 다 고쳐야하는 문제가 있다.

로그인체크로직을 공통의 관심사로 스프링의 AOP로도 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.

Filter

필터의 흐름

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(): 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다

로그인 인증 필터

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

    /**
     * 화이트 리스트를 제외한 모든 경우에 인증체크 로직 적용
     */
    private boolean isLoginCheckPath(String requestURI){
        return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
    }
}
  • 필터를 사용하기위해 필터 인터페이스를 구현한다.
  • HTTP요청이 오면 doFilter가 호출된다.
    ServletRequest request는 HTTP요청이 아닌 경우까지 고려해서 만든 것이므로, HTTP를 사용하기위해 (HttpSErvletRequest)로 다운 케스팅한다.
  • chain.doFilter(request, response)
    다음 필터가 있으면 필터를 호출하고, 없으면 서블릿을 호출한다.

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {


    @Bean
    public FilterRegistrationBean loginCheckFilter(){
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");
        return filterFilterRegistrationBean;
    }

}

스프링부트에서 FilterRegistrationBean을 사용해 등록한다.

  • setFilter(new LogFilter()) : 등록할 필터를 지정한다.
  • setOrder(1) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
  • addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다

Interceptor

스프링 인터셉터 흐름

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

스프링 인터셉터 제한

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

스프링 인터셉터 체인

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

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

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()Controller 실행 전 호출
postHandle()Controller 실행 후 DispatcherServlet으로 나갈 때 실행
afterCompletion()View(jsp)를 렌더링 후 실행

서블릿 필터의 경우 doFilter()하나만 제공되지만, 인터셉터는 컨트롤러 호출전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)와 같이 단계적으로 세분화 되있다.

로그인 인증 인터셉터

LoginCheckInterciptor

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

서블릿 필터와 비교해서 코드가 매우 간결하다. 인증이라는 것은 컨트롤러 호출 전에만 호출되면 된다. 따라서 preHandle 만 구현하면 된다

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

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

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

정리

서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술이다.
서블릿 필터와 비교해서 스프링 인터셉터가 개발자 입장에서 훨씬 편리하다는 것을 코드로 이해했을 것이다. 특별한 문제가 없다면 인터셉터를 사용하는 것이 좋다.

0개의 댓글