[Spring Security] 필터 체인
보안이 적용된 웹 요청의 일반적인 처리 흐름
- (1)에서 사용자가 보호된 리소스를 요청합니다.
- (2)에서 인증 관리자 역할을 하는 컴포넌트가 사용자의 크리덴셜(Credential)을 요청합니다.
- 사용자의 크리덴셜(Credential)이란 해당 사용자를 증명하기 위한 구체적인 수단을 의미합니다. 일반적으로는 사용자의 패스워드가 크리덴셜에 해당합니다.
- (3)에서 사용자는 인증 관리자에게 크리덴셜(Credential)을 제공합니다.
- (4)에서 인증 관리자는 크리덴셜 저장소에서 사용자의 크리덴셜을 조회합니다.
- (5)에서 인증 관리자는 사용자가 제공한 크리덴셜과 크리덴셜 저장소에 저장된 크리덴셜을 비교해 검증 작업을 수행합니다.
- (6) 유효한 크리덴셜이 아니라면 Exception을 throw합니다.
- (7) 유효한 크리덴셜이라면 (8)에서 접근 결정 관리자 역할을 하는 컴포넌트는 사용자가 적절한 권한을 부여받았는지 검증합니다.
- (9) 적절한 권한을 부여 받지 못한 사용자라면 Exception을 throw합니다.
- (10) 적절한 권한을 부여 받은 사용자라면 보호된 리소스의 접근을 허용합니다.
웹 요청에서의 서블릿 필터와 필터 체인의 역할
서블릿 기반 애플리케이션의 경우, 애플리케이션의 엔드포인트에 요청이 도달하기 전에 중간에서 요청을 가로챈 후 어떤 처리를 할 수 있는 적절한 포인트를 제공하는데 그것은 바로 서블릿 필터(Servlet Filter)이다.
서블릿 필터는 하나 이상의 필터들을 연결해 필터 체인(Filter Chain)을 구성할 수 있다.
개발자가 생성한 서블릿 필터에서 개발자가 작성한 특별한 작업들을 수행한 뒤, HttpServlet을 거쳐 DispatcherServlet에 요청이 전달되며, 반대로 DispatcherServlet에서 전달한 응답에 대해 역시 특별한 작업을 수행할 수 있다.
DelegatingFilterProxy 와 FilterChainProxy 클래스는 Filter 인터페이스를 구현하기 때문에 엄연히 서블릿 필터로써의 역할을 한다.
DelegatingFilterProxy
서블릿 필터와 연결되는 Spring Security만의 필터를 ApplicationContext에 Bean으로 등록한 후에 이 Bean들을 이용해서 보안과 관련된 작업들을 처리하게 되는데, DelegatingFilterProxy가 Bean으로 등록된 Spring Security의 필터를 사용하는 시작점이다.
FilterChainProxy
Spring Security의 Filter Chain은 보안을 위한 작업을 처리하는 필터의 모음이다. 이 Filter의 진입점이 FilterChainProxy이다.
Spring Security의 Filter Chain은 URL 별로 여러개 등록할 수 있으며, 가장 먼저 매칭된 Filter Chain을 실행한다.
Filter 구현
Servlet Filter 인터페이스를 구현한 Filter 클래스
import javax.servlet.*;
import java.io.IOException;
public class FirstFilter implements Filter {
/** 생성한 Filter에 대한 초기화 작업을 진행한다. */
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
System.out.println("FirstFilter 생성됨");
}
/** .doFilter() 메서드로 Filter 로직을 구현한다. */
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("========First 필터 시작========");
chain.doFilter(request, response);
System.out.println("========First 필터 종료========");
}
/** Filter가 컨테이너에서 종료될 때 호출하여, Filter가 사용한 자원을 반납하는 처리 등의 로직을 구현한다. */
@Override
public void destroy() {
System.out.println("FirstFilter Destory");
Filter.super.destroy();
}
}
Servlet Filter 인터페이스를 구현한 두번째 Filter 클래스
import javax.servlet.*;
import java.io.IOException;
public class SecondFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
System.out.println("SecondFilter가 생성되었습니다.");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("==========Second 필터 시작==========");
chain.doFilter(request, response);
System.out.println("==========Second 필터 종료==========");
}
@Override
public void destroy() {
System.out.println("SecondFilter가 사라집니다.");
Filter.super.destroy();
}
}
FirstFilter를 적용하기 위한 FilterConfiguration 구성
import book.study.security.FirstFilter;
import book.study.security.SecondFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Config {
@Bean
public FilterRegistrationBean<FirstFilter> firstFilterRegister() {
FilterRegistrationBean<FirstFilter> registrationBean = new FilterRegistrationBean<>(new FirstFilter());
registrationBean.setOrder(1); // 메서드 실행 첫번째 순서
return registrationBean;
}
@Bean
public FilterRegistrationBean<SecondFilter> secondFilterRegister() {
FilterRegistrationBean<SecondFilter> registrationBean = new FilterRegistrationBean<>(new SecondFilter());
registrationBean.setOrder(2); // 메서드 실행 두번째 순서
return registrationBean;
}
}
FilterRegistrationBean의 생성자로 Filter 인터페이스의 구현 객체를 넘겨주는 형태로 Servlet Filter를 등록할 수 있다.
.setOrder() 메서드 혹은 @Order 애너테이션으로 Filter의 순서를 지정할 수 있다.