spring/spring security

Spring Security 기본 개념 공부 7. (사용자 커스텀 필터 작성 방법)

대기업 가고 싶은 공돌이 2024. 6. 1. 01:35

Section 8.

스프링 시큐리티 필터

  • 기본적으로 스프링 시큐리티엔 수많은 필터들이 정의 돼있다.
  • 그 필터들의 구조와 흐름을 알아야 적절한 위치에 적절한 로직을 지닌 커스텀 필터를 삽입할 수 있다.

커스텀 필터 작성하는 방법

  • 커스텀 필터를 작성하기 위해서는 우선 Filter 인터페이스를 상속받아야한다.
  • Filter 인터페이스를 상속받은 후 doFilter 메소드 내부에서 실행하고자하는 모든 로직을 정의하면 된다.
  • doFilter 메소드의 인자로는 세가지가 있다.
      1. ServletRequest, 엔드유저로부터 오는 HTTP request이다.
      2. ServeletResponse, 엔드 유저에게 돌려보내는 HTTP response이다.
      3. filterChin, 필터들의 조합으로서 사용가능한 모든 필터로부터 다음 필터가 무엇인지 찾아내는 역할을 한다.

커스텀 필터를 어떻게 filterChain 내부에 주입할 수 있을까요?

  • 세 가지 메소드로 주입이 가능하다.
      1. addFilterBefore(filter,class), 특정 class(filter) 이전 위치에 필터를 삽입시키는 메소드다.
      2. addFilterBefore(filter,class), 특정 class(filter) 이후 위치에 필터를 삽입시키는 메소드다.
      3. 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에서만 필터가 작동하지 않게 만들수도 있다.