Spring

[SpringBoot] Filter, Interceptor 개념 정리 및 로그인 처리

꾸준함. 2021. 8. 1. 23:07

개요

기존 게시글에서는 쿠키와 세션 개념을 정리하면서 이 두 개념을 이용하여 로그인 관련 코드 적용도 해봤습니다.

(https://jaimemin.tistory.com/1885, https://jaimemin.tistory.com/1886)

하지만, 로그인 처리를 할 때 쿠키와 세션만으로는 해결 안 되는 점이 있습니다.

예를 들자면, 로그인을 해야지만 접근 가능한 페이지들이 있을 때 쿠키와 세션만 적용했을 때는 해당 페이지 url을 직접 쳐서 들어갈 경우 그대로 화면이 노출된다는 문제점이 있습니다.

사실 시스템 요구사항에서는 로그인하지 않은 상태에서 해당 페이지들에 접근할 경우 우선 로그인 페이지로 리다이렉트 한 후 다시 그 페이지로 리다이렉트 하는 방식을 원할 것입니다.

여기서 필터와 인터셉터가 쓰이는데 필터와 인터셉터는 웹과 관련된 공통 관심사를 처리할 때 사용됩니다. (AOP와 유사)

 

저를 포함해서 필터와 인터셉터의 차이를 혼동하는 사람들이 많다는 것을 실무 개발을 하면서 느꼈습니다.

따라서, 이번 게시글을 통해 필터와 인터셉터의 차이에 대해 확실히 정리해보겠습니다.

 

1. 필터

  • 정확히 명칭은 서블릿 필터 (스프링에서 제공하는 기능이 아님)
  • 필터를 적용할 경우 필터가 호출된 다음에 서블릿이 호출
  • 따라서, 모든 고객의 요청 로그를 남기는 요구사항이 있을 경우 필터를 사용
  • 기존 Spring MVC 구조를 떠올려보면 아래와 같은 구조를 그릴 수 있음 (https://jaimemin.tistory.com/1820)

 

 

구조를 보면 DispatcherServlet이 호출되기 전에 필터를 거치는 것을 확인할 수 있습니다.

 

1.1 필터 체인

  • 필터는 체인으로 구성되며, 중간에 필터를 자유롭게 추가 및 제거할 수 있음
  • 예를 들어 로그를 남기는 필터를 먼저 적용한 후 로그인 여부를 체크하는 필터를 이후에 추가할 수 있음
  • ex) HTTP 요청 -> WAS -> 필터 1 -> 필터 2 ->... -> 필터 N -> 디스패처 서블릿 -> 컨트롤러

 

1.2 필터의 역할

  • 필터는 적절하지 않은 요청이 들어올 경우 HTTP 요청이 서블릿까지 도달하지 못하게 하는 역할이 있음
  • 따라서, Spring Security를 적용할 때 필터에 적용하기도 함
  • 정상적인 요청: HTTP 요청 -> WAS -> 필터1 -> 필터 2 ->... -> 필터 N -> 디스패처 서블릿 -> 컨트롤러
  • 잘 못된 요청: HTTP 요청 -> WAS -> 필터1 -> 필터 2 -> 서블릿 호출 X 

 

1.3 필터 인터페이스 살펴보기

 

/**
* A filter is an object that performs filtering tasks on either the request to
* a resource (a servlet or static content), or on the response from a resource,
* or both. <br>
* <br>
* Filters perform filtering in the <code>doFilter</code> method. Every Filter
* has access to a FilterConfig object from which it can obtain its
* initialization parameters, a reference to the ServletContext which it can
* use, for example, to load resources needed for filtering tasks.
* <p>
* Filters are configured in the deployment descriptor of a web application
* <p>
* Examples that have been identified for this design are<br>
* 1) Authentication Filters <br>
* 2) Logging and Auditing Filters <br>
* 3) Image conversion Filters <br>
* 4) Data compression Filters <br>
* 5) Encryption Filters <br>
* 6) Tokenizing Filters <br>
* 7) Filters that trigger resource access events <br>
* 8) XSL/T filters <br>
* 9) Mime-type chain Filter <br>
*
* @since Servlet 2.3
*/
public interface Filter {
/**
* Called by the web container to indicate to a filter that it is being
* placed into service. The servlet container calls the init method exactly
* once after instantiating the filter. The init method must complete
* successfully before the filter is asked to do any filtering work.
* <p>
* The web container cannot place the filter into service if the init method
* either:
* <ul>
* <li>Throws a ServletException</li>
* <li>Does not return within a time period defined by the web
* container</li>
* </ul>
* The default implementation is a NO-OP.
*
* @param filterConfig The configuration information associated with the
* filter instance being initialised
*
* @throws ServletException if the initialisation fails
*/
public default void init(FilterConfig filterConfig) throws ServletException {}
/**
* The <code>doFilter</code> method of the Filter is called by the container
* each time a request/response pair is passed through the chain due to a
* client request for a resource at the end of the chain. The FilterChain
* passed in to this method allows the Filter to pass on the request and
* response to the next entity in the chain.
* <p>
* A typical implementation of this method would follow the following
* pattern:- <br>
* 1. Examine the request<br>
* 2. Optionally wrap the request object with a custom implementation to
* filter content or headers for input filtering <br>
* 3. Optionally wrap the response object with a custom implementation to
* filter content or headers for output filtering <br>
* 4. a) <strong>Either</strong> invoke the next entity in the chain using
* the FilterChain object (<code>chain.doFilter()</code>), <br>
* 4. b) <strong>or</strong> not pass on the request/response pair to the
* next entity in the filter chain to block the request processing<br>
* 5. Directly set headers on the response after invocation of the next
* entity in the filter chain.
*
* @param request The request to process
* @param response The response associated with the request
* @param chain Provides access to the next filter in the chain for this
* filter to pass the request and response to for further
* processing
*
* @throws IOException if an I/O error occurs during this filter's
* processing of the request
* @throws ServletException if the processing fails for any other reason
*/
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
/**
* Called by the web container to indicate to a filter that it is being
* taken out of service. This method is only called once all threads within
* the filter's doFilter method have exited or after a timeout period has
* passed. After the web container calls this method, it will not call the
* doFilter method again on this instance of the filter. <br>
* <br>
*
* This method gives the filter an opportunity to clean up any resources
* that are being held (for example, memory, file handles, threads) and make
* sure that any persistent state is synchronized with the filter's current
* state in memory.
*
* The default implementation is a NO-OP.
*/
public default void destroy() {}
}
view raw .java hosted with ❤ by GitHub

 

필터 인터페이스

  • 인터페이스에는 세 가지 메서드가 있고 init과 destory 메서드 같은 경우 default 키워드가 있기 때문에 필요가 없을 경우 별도로 정의하지 않아도 됨
  • 개요에서도 언급했드시 웹 관련 공통사항을 처리하기 때문에 파라미터로 ServletRequest와 ServletResponse 객체가 제공되는 것을 확인할 수 있음
    • 스프링 프레임워크는 확장성을 위해 ServletRequest와 ServletResponse 객체로 제공했지만 대부분 HttpServletRequest와 HttpServletResponse를 사용하기 때문에 다운 캐스팅하여 사용하면 됨

 

메서드 간단 설명

  • init() 메서드: 필터 초기화 메서드이며 서블릿 컨테이너가 생성될 때 호출
  • doFilter() 메서드: 고객의 요청이 올 때마다 해당 메서드가 호출되며 메인 로직을 이 메서드에 구현하면 됨
  • destory() 메서드: 필터 종료 메서드이며 서블릿 컨테이너가 소멸될 때 호출

 

1.4 로그인 여부 확인하는 필터 예시

  • 앞선 게시글에서는 각 컨트롤러마다 로그인 여부를 확인했는데 필터를 적용하면서 반복되는 공통 코드를 한번에 처리할 수 있음
  • 로그인 확인 여부 체크하는 필터를 구현한 뒤 @Configuration에 필터를 등록해주면 적용 가능
  • 아래 코드 참고

 

LoginCheckFilter.java

 

@Slf4j
public class LoginCheckFilter implements Filter {
private static final String SESSION_ID = "sessionId";
private static final String[] whitelist = {"/", "/login", "/logout", "/css/*", "/*.ico", ...};
@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(SESSION_ID) == null) {
log.info("미인증 사용자 요청 {}", requestURI);
// 로그인 페이지로 redirect
// 로그인 시 다시 해당 페이지로 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);
}
}
view raw .java hosted with ❤ by GitHub

 

WebConfig.java

 

@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<>();
filterRegistration.setFilter(new LoginCheckFilter());
filterRegistration.setOrder(1); // 필터 체인 순서
filterRegistration.addUrlPatterns("/*"); // 모든 요청에 대해 다 필터 적용
return filterRegistration;
}
@Bean
public FilterRegistrationBean exampleFilter() {
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<>();
filterRegistration.setFilter(new ExampleFilter());
filterRegistration.setOrder(2); // 필터 체인 순서
filterRegistration.addUrlPatterns("/*"); // 모든 요청에 대해 다 필터 적용
return filterRegistration;
}
}
view raw .java hosted with ❤ by GitHub

 

코드 부연설명

  • LoginCheckFilter 내 whitelist 같은 경우 로그인하지 않아도 접근할 수 있는 자원들 목록
  • whitelist에 포함되지 않은 요청 URL들은 로그인이 필수인 상황이므로 만약 로그인이 되어있지 않은 상태에서 요청이 올 경우 필터가 해당 요청을 튕겨내고 login 페이지로 리다이렉트 시켜야 함
    • 이때, 기존 요청을 쿼리 파라미터로 redirectURL로 지정함으로써 로그인한 이후에는 기존 요청 페이지로 리다이렉트 될 수 있도록 처리하는 것이 고객 입장에서 편리 (httpResponse.sendRedirect("/login?redirectURL=" + requestURI);)
    • 더 이상 필터 체인을 타지 않기 위해 return; 문을 넣는 것이 중요
  • WebConfig 내 필터를 등록하기 위해서는 각 필터를 빈으로 등록해줘야 함
    • 이후 설명하겠지만 인터셉터의 경우 하나의 메서드 내 여러 인터셉터를 등록할 수 있고 필터의 경우 필터 개수만큼 메서드를 선언하여 빈으로 등록해줘야 함
    • 모든 요청에 로그인 필터를 적용한 뒤 whitelist에 포함되었는지 여부는 로그인 필터 내부 로직에서 확인 (인터셉터의 경우 이 같은 처리를 편리하게 적용하기 위해 보다 편한 메서드 제공)

 

2. 인터셉터

  • 필터는 서블릿이 제공하는 기술이지만 스프링 인터셉터는 스프링 MVC가 제공하는 기술
  • 웹 관련 공통 관심 사항을 처리한다는 점에서는 필터와 비슷
  • 인터셉터는 필터와 달리 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출됨
  • 기존 Spring MVC 구조를 떠올려보면 아래와 같은 구조를 그릴 수 있음 (https://jaimemin.tistory.com/1820)

 

 

2.1 스프링 인터셉터 체인

  • 스프링 인터셉터 또한 체인으로 구성되며 중간에 자유롭게 인터셉터를 추가 및 제거 가능
  • 예를 들어 로그를 남기는 인터셉터를 먼저 적용한 후 로그인 여부를 체크하는 인터셉터를 이후에 추가할 수 있음
  • ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 1 -> 인터셉터 2 ->... -> 인터셉터 N -> 컨트롤러

 

2.2 스프링 인터셉터의 역할

  • 스프링 인터셉터 또한 필터처럼 적절하지 않은 요청이 들어올 경우 HTTP 요청이 서블릿까지 도달하지 못하게 하는 역할이 있음
  • 따라서, Spring Security를 적용할 때 스프링 인터셉터에 적용하기도 함
  • 단, 필터는 서블릿에 도달하기 전에 호출되고 스프링 인터셉터는 서블릿에서 컨트롤러를 호출하기 전에 호출됨
  • 정상적인 요청: HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 인터셉터 1 -> 인터셉터 2 ->... -> 인터셉터 N -> 컨트롤러
  • 잘 못된 요청: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 1 -> 인터셉터 2 -> 컨트롤러 호출 안됨 

 

2.3 스프링 인터셉터 인터페이스 살펴보기

 

public interface HandlerInterceptor {
/**
* Intercept the execution of a handler. Called after HandlerMapping determined
* an appropriate handler object, but before HandlerAdapter invokes the handler.
* <p>DispatcherServlet processes a handler in an execution chain, consisting
* of any number of interceptors, with the handler itself at the end.
* With this method, each interceptor can decide to abort the execution chain,
* typically sending an HTTP error or writing a custom response.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation returns {@code true}.
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return {@code true} if the execution chain should proceed with the
* next interceptor or the handler itself. Else, DispatcherServlet assumes
* that this interceptor has already dealt with the response itself.
* @throws Exception in case of errors
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
/**
* Intercept the execution of a handler. Called after HandlerAdapter actually
* invoked the handler, but before the DispatcherServlet renders the view.
* Can expose additional model objects to the view via the given ModelAndView.
* <p>DispatcherServlet processes a handler in an execution chain, consisting
* of any number of interceptors, with the handler itself at the end.
* With this method, each interceptor can post-process an execution,
* getting applied in inverse order of the execution chain.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation is empty.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the handler (or {@link HandlerMethod}) that started asynchronous
* execution, for type and/or instance examination
* @param modelAndView the {@code ModelAndView} that the handler returned
* (can also be {@code null})
* @throws Exception in case of errors
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
/**
* Callback after completion of request processing, that is, after rendering
* the view. Will be called on any outcome of handler execution, thus allows
* for proper resource cleanup.
* <p>Note: Will only be called if this interceptor's {@code preHandle}
* method has successfully completed and returned {@code true}!
* <p>As with the {@code postHandle} method, the method will be invoked on each
* interceptor in the chain in reverse order, so the first interceptor will be
* the last to be invoked.
* <p><strong>Note:</strong> special considerations apply for asynchronous
* request processing. For more details see
* {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
* <p>The default implementation is empty.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the handler (or {@link HandlerMethod}) that started asynchronous
* execution, for type and/or instance examination
* @param ex any exception thrown on handler execution, if any; this does not
* include exceptions that have been handled through an exception resolver
* @throws Exception in case of errors
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
view raw .java hosted with ❤ by GitHub

 

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

  • 인터페이스에는 세 가지 메서드가 있고 필터와 달리 세 가지 메서드 모두 default 키워드가 있음
  • 서플릿 필터의 경우 단순하게 doFilter 메서드만 제공되었지만 스프링 인터셉터의 경우 컨트롤러 호출 전에 호출되는 preHandle 메서드, 컨트롤러 호출 후 호출되는 postHandle 메서드, 그리고 요청이 완료된 이후 호출되는 afterCompletion 메서드가 단계적으로 잘 세분화되어 필터보다 많은 기능을 제공
  • 서블릿 필터의 경우 단순히 request, response만 제공했지만 스프링 인터셉터는 어떤 컨트롤러(handler)가 호출되었는지에 관한 호출 정보도 받을 수 있음
  • 또한, 어떤 ModelAndView가 반환되었는지 응답 정보도 받을 수 있음
  • 정리를 하자면 스프링 인터셉터는 필터와 비슷하게 작동하지만 호출 시점이 다르고 인터셉터가 필터보다 더 많은 기능을 제공

 

메서드 간단 설명

  • preHandle() 메서드: 컨트롤러 호출 전, 더 정확히 말하자면 HandlerAdapter 호출 전에 호출
    • preHandle의 반환 값이 true일 경우 다음 체인으로 진행이 되고, false일 경우 더 이상 진행이 되지 않으며 핸들러 어댑터 또한 호출이 되지 않음
  • postHandle() 메서드: 컨트롤러 호출 후, 더 정확히 말하자면 HandlerAdapter 호출 후에 호출
  • afterCompletion() 메서드: 뷰가 렌더링 된 이후에 호출되며 try catch finally 절에서 finally처럼 무조건 호출됨

 

2.4 스프링 인터셉터에서 예외 발생할 경우

  • 컨트롤러에서 예외가 발생할 경우 postHandle 메서드는 호출되지 않음
  • 앞서 언급했듯이 afterCompletion 메서드는 무조건 호출되므로 예외 발생 시 파라미터로 어떤 예외가 발생했는지 알 수 있으며 로그로 출력 가능
    • 필터의 경우 예외를 로깅하기 위해서는 톰캣까지 예외를 보내줘야 하지만 인터셉터의 경우 바로 로깅 가능

 

2.5 로그인 여부 확인하는 스프링 인터셉터 예시

  • 필터처럼 반복되는 공통 코드를 한 번에 처리할 수 있음
  • 로그인 확인 여부 체크하는 필터를 구현한 뒤 @Configuration에 필터를 등록해주면 적용 가능
    • 필터와 달리 WebConfig가 WebMvcConfigurer 인터페이스를 구현해야 함
  • 아래 코드 참고

 

LoginCheckInterceptor.java

 

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
private static final String SESSION_ID = "sessionId";
@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(SESSION_ID) == null) {
log.info("미인증 사용자 요청");
// 로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
view raw .java hosted with ❤ by GitHub

 

WebConfig.java

 

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor())
.order(1) // 인터셉터 체인 순서
.addPathPatterns("/**") // 모든 requestURL에 대해 적용
.excludePathPatterns("/" // 제외하고 싶은 whitelist
, "/login"
, "/logout"
, "/css/**"
, "/*.ico"
, "/error");
registry.addInterceptor(new ExampleInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/css/**"
, "/*.ico"
, "/error");
}
}
view raw .java hosted with ❤ by GitHub

 

코드 부연설명

  • 로그인 확인 여부는 컨트롤러 호출 전에 이루어지므로 preHandle만 구현
  • 필터와 마찬가지로 만약 로그인이 되어있지 않은 상태에서 요청이 올 경우 필터가 해당 요청을 튕겨내고 login 페이지로 리다이렉트 시켜야 함
    • 이때, 기존 요청을 쿼리 파라미터로 redirectURL로 지정함으로써 로그인한 이후에는 기존 요청 페이지로 리다이렉트 될 수 있도록 처리하는 것이 고객 입장에서 편리 (httpResponse.sendRedirect("/login?redirectURL=" + requestURI);)
    • 여기서는 whitelist를 인터셉터 내에서 명시하지 않았다는 것을 주목
  • WebConfig 내 인터셉터를 등록하기 위해서는 인터셉터들을 addInterceptors 메서드에서 등록해주면 됨
    • 필터와 달리 단 하나의 메서드에서 여러 개 인터셉터 등록 가능
    • 필터와 달리 excludePathPatterns 메서드를 지원하여 whitelist를 WebConfig에서 등록 가능

 

최종 정리

  • 필터는 서블릿에서 제공하며 디스패처 서블릿 호출되기 전에 호출됨
  • 스프링 인터셉터는 Spring MVC에서 제공되며 컨트롤러, 정확히 말하면 HandlerAdapter 호출 직전에 호출됨
  • 이처럼 필터와 인터셉터는 호출 시점이 다르지만 웹 공통 관심 사항을 처리한다는 점은 동일
  • 하지만, 스프링 인터셉터가 필터보다 개발자 친화적으로 다양한 기능을 제공하므로 서블릿이 호출되기 직전에 처리해야 하는 로직이 아니라면 스프링 인터셉터를 활용하는 것을 추천

 

비고

 

PathPattern (Spring Framework 5.3.9 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

 

출처

인프런 스프링 MVC 2편 (김영한 강사님)

반응형