TIL - doFilter 메소드에 대해서
개발하면서 매우 궁금했던 게 바로 chain.doFilter(request, response);
메소드였다. JwtAuthorizationFilter
에 존재하는 이 doFilter
는 도대체 무엇을 하는 애일까? 내 첫 번째 TIL에서 공부했던 게 바로 doFilter 안의 인자로 들어가는 HttpServletRequest request
와 HttpServletResponse response
였는데, 얘들은 일종의 요청과 응답을 나타내는 객체라는 것을 알게되었다. 그러나 이제 궁금한 점은 그렇다면 요청과 응답을 동시에 가지고 있는 doFilter 메소드는 무엇을 하는 것 인지였다.
oracle에 따르면, 필터는 리소스(서블릿 또는 정적 콘텐츠)에 대한 요청이나 리소스의 응답, 또는 둘 다에 대해 필터링 작업을 수행하는 객체라고 한다. 여기서 약간의 힌트를 얻을 수 있다. 우리는 JWT 토큰을 request에 대해서만 검증하면 되긴 하지만, 일반적으로 필터의 일종인 JwtAuthorizationFilter
은 리소스의 요청/응답 모두에 필터링을 할 수 있는 객체인 것이다. 조금 더 구체적인 메소드의 동작 방식은 다음과 같다:
- request를 검토한다.
- 선택적으로 request 객체(
HttpServletRequest request
)를 사용자 정의 구현으로 래핑하여 입력에서 contents 또는 header를 필터링한다. - 선택적으로 response 객체(
HttpServletResponse response
)를 사용자 정의 구현으로 래핑하여 출력에서 콘텐츠 또는 헤더를 필터링한다. - FilterChain 개체(
chain.doFilter()
)를 사용하여 체인의 다음 엔터티를 호출하거나, 또는 request 처리를 차단하기 위해 request/response 쌍을 필터 체인의 다음 엔터티로 전달하지 않는다. - 필터 체인에서 다음 엔터티를 호출한 후 응답에 헤더를 직접 설정한다. 나는 아래와 같은
JwtAuthorizationFilter
클래스에서doFilter
를 호출했었다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
/* 헤더 추출 및 정상적인 헤더인지 확인 */
String jwtHeader = request.getHeader("Authorization");
if (jwtHeader == null || !jwtHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
/* 헤더 안의 JWT 토큰을 검증해 정상적인 사용자인지 확인 */
String jwtToken = jwtHeader.substring(7);
try {
Member tokenMember = jwtTokenProvider.validJwtToken(jwtToken);
if(tokenMember != null){ //토큰이 정상일 경우
AuthDetails authDetails = new AuthDetails(tokenMember);
/* JWT 토큰 서명이 정상이면 Authentication 객체 생성 */
Authentication authentication = new UsernamePasswordAuthenticationToken(authDetails, null, authDetails.getAuthorities());
/* 시큐리티 세션에 Authentication 을 저장 */
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
} catch (TokenExpiredException e){
log.error(e + " EXPIRED_TOKEN");
//request.setAttribute("exception", ErrorCode.EXPIRED_TOKEN.getCode());
setResponse(response, ErrorCode.EXPIRED_TOKEN);
} catch (SignatureVerificationException e){
log.error(e + " INVALID_TOKEN_SIGNATURE");
setResponse(response, ErrorCode.INVALID_TOKEN_SIGNATURE);
}
}
그리고 다음과 같은 SecurityConfig
에서 필터 체인을 정의했었다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtTokenProvider(), refreshTokenService))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), jwtTokenProvider(), authDetailService))
.authorizeHttpRequests(requests -> requests
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
.anyRequest().permitAll()
);
return httpSecurity.build();
}
이 코드에 따르면, doFilter
은 JwtAuthorizationFilter
의 로직을 종료하고 필터 체인의 다음 엔티티로 넘겨주는 역할을 한다. 이러한 디자인 패턴을 Chain-of-responsibility 패턴이라고 한다. 이 체인이 종료되어야지만 요청/응답 쌍이 원래 목표했던 타겟 리소스/서블릿으로 이동이 가능한 것이다. 아마 DB와 긴밀하게 연관되어 있는 타겟 리소스/서블릿으로 이동하기 전이나 후에 데이터의 유효성을 검증하고 싶기 때문에 필터라는 것을 만든 것 같다. 프론트엔드에도 필터를 만들 수 있지만, JS 비활성화로 간단하게 무시될 수도 있기 때문이다. 그래서 필터는 아래 그림에서 볼 수 있듯이, DispatcherServlet
으로 이동하기 전에 작동한다.
여기서 HttpServletResponse response
가 무엇인지를 잠깐 보자. oracle에 따르면 이는 HTTP 관련 기능을 제공하도록 servlet에 의해 생성된 객체이며 doGet, doPost 등의 메소드에 전달되는 인수라고 한다. 상태 코드를 비롯해서, addCookie
나 addHeader
등의 다양한 메소드를 포함하고 있다.
QnA
이렇게 스터디를 진행했는데 다음과 같은 질문을 받았다.
그럼 filter가 SecurityFilterChain에 addFilter 메소드로 등록된다는 건 알겠는데, SecurityFilterChain는 어떻게 스프링에 등록되나요? 자동으로 실행 가능하게 등록되는 건가요?
이 부분은 나도 잘 모르겠어서 추가로 찾아봐야겠다는 생각이 들었다. 우선 SecurityFilterChain
의 위에 @Bean
이 있으므로 실행 가능하게 만들어지는 이유는 명확하다. 여기서 이게 중요한 것은 어떻게 이게 가장 먼저 돌아가는지를 스프링이 아는지이다. 찾아보니, 스프링 시큐리티는 서비스 설정(Configuration)에 라서 filter를 순서대로 실행할 수 있다고 한다. 서블릿 필터를 사용할 때는 당연히 web.xml에 해당 필터들을 선언해야 한다(그렇지 않으면 서블릿 컨테이너에 의해서 무시된다). web.xml에 선언된 DelegatingFilterProxy
이 바로 web.xml과 애플리케이션 컨텍스트 사이의 연결을 제공하는 역할을 한다.
<filter>
<filter-name> myFilter </filter-name>
<filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class>
</filter>
<filter-mapping>
<filter-name> myFilter </filter-name>
<url-pattern> /* </url-pattern>
</filter-mapping>
DelegatingFilterProxy
가 하는 일은 Spring 애플리케이션 컨텍스트에서 가져온 빈(springSecurityFilterChain
)을 통해 필터의 메서드를 위임하는 것이다. Spring Security의 웹 인프라는 FilterChainProxy
의 인스턴스로 위임함으로써 사용되어야 한다. 물론 필요한 각 Spring Security 필터 빈을 애플리케이션 컨텍스트 파일에 선언하고, 각 필터에 대한 해당 DelegatingFilterProxy 항목을 모두 web.xml에 추가해줄 수 있겠지만, 이는 너무 번거롭다. 이때 FilterChainProxy
를 사용하면 web.xml에 단일 항목만 추가해도 웹 보안 빈을 관리하기 위해 완전히 애플리케이션 컨텍스트 파일을 처리할 수 있다. 이는 앞서 언급한 예제와 마찬가지로 DelegatingFilterProxy를 사용하여 연결되지만, filter-name이 빈 이름 "filterChainProxy"로 설정된다. 그런 다음 아래와 같이 필터 체인은 동일한 빈 이름으로 애플리케이션 컨텍스트에 선언된다:
<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg>
<list>
<sec:filter-chain pattern="/restful/**" filters="
securityContextPersistenceFilterWithASCFalse,
basicAuthenticationFilter,
exceptionTranslationFilter,
filterSecurityInterceptor" />
<sec:filter-chain pattern="/**" filters="
securityContextPersistenceFilterWithASCTrue,
formLoginFilter,
exceptionTranslationFilter,
filterSecurityInterceptor" />
</list>
</constructor-arg>
</bean>
정리하자면 다음과 같다.
- web.xml에 선언된
DelegatingFilterProxy
은 서블릿 컨테이너의 라이프사이클(web.xml)과 애플리케이션 컨텍스트 사이의 연결을 제공한다. 즉, ApplicationContext에서 Filter의 Bean을 찾아서 호출할 수 있다. FilterChainProxy
는 SecurityFilterChain을 통해 많은 필터 인스턴스 목록에 위임을 허용하는 특별한 필터이다. FilterChainProxy는 Bean이므로 일반적으로 DelegatingFilterProxy에 래핑 가능하다.SecurityFilterChain
은 순서대로 필터를 호출한다. 호출된 필터는 이렇게 작동된다.- request를 검토한다.
- 선택적으로 request 객체(HttpServletRequest request) 또는 response 객체(HttpServletResponse response)를 사용자 정의 구현으로 래핑하여 입력/출력에서 contents 또는 header를 필터링한다.
- FilterChain 개체(chain.doFilter())를 사용하여 체인의 다음 엔터티를 호출하거나, 또는 request 처리를 차단하기 위해 request/response 쌍을 필터 체인의 다음 엔터티로 전달하지 않는다.
- 하나의 필터가 끝나면 필터 체인에서 다음 엔터티를 호출한 후 응답에 헤더를 직접 설정한다.
그림으로 표현하자면 아래와 같이 적용된다:
레퍼런스
Filter
Filter (Java EE 6 )
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) The doFilter 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 f
docs.oracle.com
What is chain.doFilter doing in Filter.doFilter method?
In a Filter.doFilter method I made this call chain.doFilter. What is doFilter doing inside a doFilter? Isn't it a recursive call?
stackoverflow.com
Java Servlet Filter with Example - GeeksforGeeks
A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.
www.geeksforgeeks.org
Architecture :: Spring Security
The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec
docs.spring.io
Demystifying Filters and Interceptors in Spring Boot
Spring Boot, a popular framework for building Java-based web applications, provides developers with powerful tools to enhance and customize the request-response lifecycle. Two such tools, filters and interceptors, often create confusion due to their simila
www.linkedin.com
[Spring] 스프링 필터의 동작과정
목적 스프링 필터의 동작과정을 이해하기 위함 목차 필터의 동작과정 1. 필터의 동작과정 1. Application의 doFilter() 서버로 요청이 들어오면 StandardWrapperValve 클래스의 invoke메서드가 실행된다. 이 메
emgc.tistory.com
HttpServletResponse 및 에러 핸들
HttpServletResponse (Java EE 6 )
void setStatus(int sc, java.lang.String sm) Deprecated. As of version 2.1, due to ambiguous meaning of the message parameter. To set a status code use setStatus(int), to send an error with a description use sendError(int, String).
docs.oracle.com
DefaultErrorAttributes (Spring Boot 3.2.4 API)
getError Return the underlying cause of the error or null if the error cannot be extracted. Specified by: getError in interface ErrorAttributes Parameters: webRequest - the source request Returns: the Exception that caused the error or null
docs.spring.io
HandlerExceptionResolver (Spring Framework 6.1.5 API)
Interface to be implemented by objects that can resolve exceptions thrown during handler mapping or execution, in the typical case to error views. Implementors are typically registered as beans in the application context. Error views are analogous to JSP e
docs.spring.io
SecurityFilterChain
SpringSecurity FilterChain이 만들어지는 과정 살펴보기
SpringSecurity FilterChain이 만들어지는 과정 살펴보기
thecodinglog.github.io
13. The Security Filter Chain
Spring Security’s web infrastructure is based entirely on standard servlet filters. It doesn’t use servlets or any other servlet-based frameworks (such as Spring MVC) internally, so it has no strong links to any particular web technology. It deals in H
docs.spring.io
https://www.baeldung.com/spring-delegating-filter-proxy
Spring Security | 01 | DelegatingFilterProxy, FilterChainProxy
이번 사이드 프로젝트를 통해 Spring Security 를 사용해보았습니다. 🍔 Spring Security , Authentication, Authorization Spring Security Reference >## Spring Security Spring
velog.io
'🗄️Backend > SpringBoot' 카테고리의 다른 글
[TodayILearn] @Scheduled 어노테이션에 대해서 (0) | 2024.05.06 |
---|---|
[TodayILearn] FilterExceptionHandler란?(feat. JWT 토큰 관련 클라이언트 에러 Filter에서 Handling해 에러 코드 반환하기) (1) | 2024.05.02 |
[TodayILearn] Spring Bean Life Cycle이란? (0) | 2024.04.08 |
[TodayILearn] 스프링 서블릿(Servlet)에 대해(HttpServletRequest, HttpServletResponse) (0) | 2024.04.03 |
Spring Boot에서 @AuthUser 커스텀 어노테이션 생성(@interface, @Target, @Retention) (0) | 2024.03.23 |