- 기본적으로 스프링 시큐리티엔 수많은 필터들이 정의 돼있다.
- 그 필터들의 구조와 흐름을 알아야 적절한 위치에 적절한 로직을 지닌 커스텀 필터를 삽입할 수 있다.
- 커스텀 필터를 작성하기 위해서는 우선 Filter 인터페이스를 상속받아야한다.
- Filter 인터페이스를 상속받은 후 doFilter 메소드 내부에서 실행하고자하는 모든 로직을 정의하면 된다.
- doFilter 메소드의 인자로는 세가지가 있다.
-
- ServletRequest, 엔드유저로부터 오는 HTTP request이다.
- ServeletResponse, 엔드 유저에게 돌려보내는 HTTP response이다.
- filterChin, 필터들의 조합으로서 사용가능한 모든 필터로부터 다음 필터가 무엇인지 찾아내는 역할을 한다.
커스텀 필터를 어떻게 filterChain 내부에 주입할 수 있을까요?
- 세 가지 메소드로 주입이 가능하다.
-
- addFilterBefore(filter,class), 특정 class(filter) 이전 위치에 필터를 삽입시키는 메소드다.
- addFilterBefore(filter,class), 특정 class(filter) 이후 위치에 필터를 삽입시키는 메소드다.
- addFilterAt(filter, class), 특정 class 위치에 필터를 삽입시킨다.
- 특정 위치에 삽입시키면 해당 위치에 있던 필터와 삽입된 필터 둘 중 하나가 랜덤으로 실행된.
- 사실상 addFilterBefore과 다름이 없다.
- 위의 메소드들은 동일한 end matches, cors configuration, csrf configuraion을 지닌 곳에서만 실행돼야한다.
Filter 인터페이스의 구조
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
default void destroy() {
}
}
- init 메소드는 처음 웹 애플리케이션이 실행되고 모든 필터들이 초기화 될때 실행되는 메소드이다.
- doFilter메소드는 필터가 호출될 때 실행되는 메소드로 이곳에 원하는 로직을 작성하면 된다.
- destroy메소드는 필터가 파괴될 때 실행되는 메소드이다.
addFilterBefore 메소드를 활용하여 커스텀 필터 추가해보기
- 로그인 이전에 입력한 값을 기반으로 특정 로직을 수행하고 싶다는 요구사항이 들어왔다.
- 우선 기본적인 흐름은 corsFilter, csrfFilter, BasicAuthenticationFilter 순으로 실행된다.
- 요구사항을 만족시키기 위해선 로그인 이전 즉 BasicAuthenticationFilter 이전에 새로운 필터를 추가해야한다.
- 유저 네임에 test가 포함되어있다면 로그인을 실패시키는 로직을 작성해보자
- 코드줄 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 다음과 같이 헤더에서 email을 추출하여 형식에 맞지 않다면 BadRequest를 반환시키는 식으로 코드를 짰다.
- 이후 configuration에 들어가 defaultSecurityFilterChain에서 addFilterBefore 메소드를 호출하면된다.
-
- 이렇게 정의해주면 된다.
- 만약 BasicAuthenticationFilter에 Before, At, After 을 모두 정의해줬다면 필터의 수행 순서는 다음과 같다.
- BeforeFilter -> (BasicAuthenticationFilter or AtFilter) -> AfterFilter의 순서로 실행된다.
.addFilterBefore(new RequestValidationBeforeFilter, BasicAuthenticationFilter.class)
커스텀 필터 구현시 Filter 인터페이스 이외의 다른 선택지
- 첫 번째는 GenericFilterBean 추상 클래스이다.
- 이 GenericFilterBean 또한 Filter 인터페이스를 구현한다.
- 장점: 구성 매개변수, 초기 매개변수, 서블릿 컨텍스트에 접근해야하는 시나리오가 있다면 해당 클래스 내부의 setEnvironment, setServletContext등의 메소드들을 활용하여 설정할 수 있으므로, 해당 시나리오에서 유용하다.
- 두 번째는 oncePerRequestFilter 추상 클래스이다.
- 해당 클래스는 GenericFilterBean추상 클래스를 확장한다.
- 장점: 요청당 무조건 한 번만 수행된다. 필터는 여러가지 이유로 한 번의 요청에서 여러번 수행이 될 수도 있다. 그러나 해당 추상클래스를 구현하면 한 번의 요청에서 무조건 한 번만 수행이된다.
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request instanceof HttpServletRequest httpRequest) {
if (response instanceof HttpServletResponse httpResponse) {
String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (!this.skipDispatch(httpRequest) && !this.shouldNotFilter(httpRequest)) {
if (hasAlreadyFilteredAttribute) {
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
this.doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
return;
}
filterChain.doFilter(request, response);
} else {
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
this.doFilterInternal(httpRequest, httpResponse, filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
} else {
filterChain.doFilter(request, response);
}
return;
}
}
- 위의 코드를 보면 이미 실행이 됐다면 바로 다음 필터로 넘기는 로직이 다 구현돼있다.
- 그렇다면 OnceperRequestFilter를 구현할때 우리의 비즈니스 로직은 어디에 구현해야할까?
- 바로 doFilterInternal메소드 내부에 비즈니스 로직을 구현하면 된다.
- 또한 특별한 메소드가 하나 더 존재하는데
- ShouldNotFilter 메소드를 활용하면 특정 Api 경로에 대해 해당 필터를 실행하고 싶지 않다면 내부 정보를 구현하여 특정 Api에서만 필터가 작동하지 않게 만들수도 있다.