JWT를 구현하는 Security 구현에 대한 글들을 보면 JWT filter 구현시, OncePerRequestFilter를 사용하는 경우가 많다.
(사실 단 한 번도 빠짐없이 모두 OncePerRequestFilter를 사용하는 것으로 보였다.)
Spring Security 기능에 필터를 추가하고자 한다면 Filter를 사용하면 된다.
그럼에도 불구하고 OncePerRequestFilter를 사용하는 이유는 뭘까?
OncePErRequestFilter는 무엇일까?
(나는 지금까지 왜 이걸 궁금해하지 않고 그냥 복붙 하여 쓰고만 있었을까..)
OncePerRequestFilter를 알기 전에 먼저 GenericFilterBean라는 추상 클래스에 대해 먼저 알아보는 게 좋겠다.
(OncePerRequestFilter는 GenericFilterBean을 상속받기 때문 받는다.)
GenericFilterBean
GenericFilterBean의 설명을 보면 web.xml이나 구성한 모든 설정 매개변수, 초기 매개변수 및 서블릿 컨텍스트 매개변수의 세부 정보를 제공한다고 명시되어 있다.
GenericFilterBean은 Filter를 구현하며 이외에도 BeanNameAware, EnvironmentAware, EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean 클래스들의 구현체로 사용된다.
그렇기 때문에 커스텀 필터 내에서 위와 같은 구성, 초기 매개변수 또는 서블릿 컨텍스트에 접근해야 되는 케이스가 존재한다면, 해당 추상 클래스를 활용할 경우 유용할 수 있다.
/**
* Simple base implementation of {@link jakarta.servlet.Filter} which treats
* its config parameters ({@code init-param} entries within the
* {@code filter} tag in {@code web.xml}) as bean properties.
*
* <p>A handy superclass for any type of filter. Type conversion of config
* parameters is automatic, with the corresponding setter method getting
* invoked with the converted value. It is also possible for subclasses to
* specify required properties. Parameters without matching bean property
* setter will simply be ignored.
*
* <p>This filter leaves actual filtering to subclasses, which have to
* implement the {@link jakarta.servlet.Filter#doFilter} method.
*
* <p>This generic filter base class has no dependency on the Spring
* {@link org.springframework.context.ApplicationContext} concept.
* Filters usually don't load their own context but rather access service
* beans from the Spring root application context, accessible via the
* filter's {@link #getServletContext() ServletContext} (see
* {@link org.springframework.web.context.support.WebApplicationContextUtils}).
*
* @author Juergen Hoeller
* @since 06.12.2003
* @see #addRequiredProperty
* @see #initFilterBean
* @see #doFilter
*/
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,
EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {
...
}
해당 클래스에서 제공하는 메서드들을 보면 환경 세부 정보, 필터 구성 세부 정보, 서블릿 컨텍스트 세부 정보 및 초기 매개변수를 제공하는 여러 가지 메서드가 존재한다.
이러한 세부 정보는 보통 배포 서술자 내에서 정의하기에 해당 클래스에서 쉽게 사용할 수 있다.
위 클래스를 사용하면 매개변수를 읽기 위한 비즈니스 로직을 모두 작성할 필요가 없다. GenericFilterBean을 활용하자.
OncePerRequestFilter
/**
* Filter base class that aims to guarantee a single execution per request
* dispatch, on any servlet container. It provides a {@link #doFilterInternal}
* method with HttpServletRequest and HttpServletResponse arguments.
*
* <p>A filter may be invoked as part of a
* {@link jakarta.servlet.DispatcherType#REQUEST REQUEST} or
* {@link jakarta.servlet.DispatcherType#ASYNC ASYNC} dispatches that occur in
* separate threads. A filter can be configured in {@code web.xml} whether it
* should be involved in async dispatches. However, in some cases servlet
* containers assume different default configuration. Therefore, subclasses can
* override the method {@link #shouldNotFilterAsyncDispatch()} to declare
* statically if they should indeed be invoked, <em>once</em>, during both types
* of dispatches in order to provide thread initialization, logging, security,
* and so on. This mechanism complements and does not replace the need to
* configure a filter in {@code web.xml} with dispatcher types.
*
* <p>Subclasses may use {@link #isAsyncDispatch(HttpServletRequest)} to
* determine when a filter is invoked as part of an async dispatch, and use
* {@link #isAsyncStarted(HttpServletRequest)} to determine when the request
* has been placed in async mode and therefore the current dispatch won't be
* the last one for the given request.
*
* <p>Yet another dispatch type that also occurs in its own thread is
* {@link jakarta.servlet.DispatcherType#ERROR ERROR}. Subclasses can override
* {@link #shouldNotFilterErrorDispatch()} if they wish to declare statically
* if they should be invoked <em>once</em> during error dispatches.
*
* <p>The {@link #getAlreadyFilteredAttributeName} method determines how to
* identify that a request is already filtered. The default implementation is
* based on the configured name of the concrete filter instance.
*
* @author Juergen Hoeller
* @author Rossen Stoyanchev
* @author Sam Brannen
* @since 06.12.2003
*/
public abstract class OncePerRequestFilter extends GenericFilterBean {
...
}
Filter를 사용하여 Spring Security FilterChain에 구성하려고 할 때, 기본적으로 Spring Security 또는 스프링은 해당 필터가 요청 당 한 번만 실행된다고 보장하지 않는다고 한다. 여러 이유가 겹치면 서블릿 컨테이너가 필터를 여러 번 호출할 수도 있다는 것..!
그렇기 때문에 OncePerRequestFilter를 상속하여 커스텀 필터를 정의하는 것을 권장하고 있다.
OncePerRequestFilter라는 이름도 해당 필터가 각 요청에 대해서 반드시 한 번만 실행되도록 보장하는 것을 의미한다.
해당 클래스의 doFilter부분을 보면, 필터가 이미 실행되었는지의 여부를 결정하기 위해 필요한 많은 로직이 있다.
@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!((request instanceof HttpServletRequest httpRequest) && (response instanceof HttpServletResponse httpResponse))) {
throw new ServletException("OncePerRequestFilter only supports HTTP requests");
}
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else if (hasAlreadyFilteredAttribute) {
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
return;
}
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else {
// Do invoke this filter...
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(httpRequest, httpResponse, filterChain);
}
finally {
// Remove the "already filtered" request attribute for this request.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
필터가 이미 실행되었다면 필터를 호출하지 않고 진행한다.
지금까지 doFilter를 통해 비즈니스 로직을 작성했는데, OncePerRequestFilter에서 필터 실행 여부를 확인하고 있으므로 OncePerRequestFilter의 경우 doFilterInternal을 통해 로직을 작성하면 된다.
OncePerRequestFilter의 경우에는 우리의 비즈니스로직을 abstract으로 선언되어 있는 doFilterInternal이라는 메서드에 작성해야 한다.
protected abstract void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException;
추가적으로 해당 필터에는 다른 유형의 유용한 메서드들이 존재한다.
shouldNotFilter 어떤 이유에서인지 web API나 일부 REST API경로에 대해 해당 필터를 실행하고 싶지 않다면 해당 세부 정보를 정의하고 조건에 따라 true를 반환한다.
여담으로 BasicAuthenticationFilter는 OncePerRequestFilter를 상속받아 구현한 필터이므로 참고하면 좋을 것 같다!
'Server > Security' 카테고리의 다른 글
[Spring Security] UserDetails Class에 대하여 (0) | 2024.01.23 |
---|---|
[Spring Security] Security 종속성 추가 후, 자동 로그인 화면이 뜨는 이유 (0) | 2024.01.21 |