Spring Security: Filter

Ajisai·2024년 2월 17일
0

Spring Security

목록 보기
1/7

https://docs.spring.io/spring-security/reference/servlet/architecture.html

  • 이 글은 그냥 공식 문서를 요약한 글이다.
    처음에 스프링 시큐리티의 필터가 뭔지 모르겠어서 개인적으로 정리한 내용이므로 그냥 문서를 읽어도 상관없을 것이다.
  • 때문에 Security filter와 다른 Filter의 차이, Interceptor와의 차이 등은 다루지 않는다(사실 잘 모름)

Filter

  • Spring Security의 Servlet에 대한 지원(Imperative programming)은 Servlet Filter를 기반으로 한다.
  • Filter의 역할은 request를 필터링하는 것

FilterChain

  • 여러 개의 필터로 구성된다.
  • 맨 마지막에는 항상 Servlet이 있다.
  • 즉 요청이 들어오면 Filter들을 거(치면서 걸러내지는 작업을 거)쳐 Servlet에 도달하는 것이다.

Client가 요청을 보내면

Spring container는 FilterChain을 생성하며(즉 FilterChain도 Bean이다), FilterChain은 다음을 포함한다.

  • Filter
  • Servlet

그리고 이 FilterServlet은 request URI를 기반으로 HttpServletRequest를 처리해야 한다.

Spring MVC application에서 ServletDispatcherServlet의 instance다.

많아봤자 Servlet 한 개가 단일 HttpServletRequestHttpServletResponse만 handling할 수 있지만, 다음의 경우에서는 여러 개의 Filter도 사용할 수 있다.

  • downstream의 Filter instance 또는 Servlet instance가 호출되지 않도록 한다. 즉 뒤에 Filter나 Servlet이 더 있어도 현재 Filter 선에서 컷하고 응답할 수 있다(응답은 HttpServletResponse)
  • HttpServletRequestHttpServletResponse는 downstream의 Filter를 통해 수정된다.
public void doFilter(
        ServletRequest request,
        ServletResponse response,
        FilterChain chain
) {
	  // do something before the rest of the application
    chain.doFilter(request, response); // invoke the rest of the application
    // do something after the rest of the application
}
  • Filter는 downstream의 Filter, 그러니까 자기보다 밑에 있는(자신 다음의) Filter 또는 Servlet에만 영향을 줄 수 있다.
    따라서 Filter가 호출되는 순서가 매우매우 중요하다.

DelegatingFilterProxy

  • Filter의 구현체
  • Servlet container의 생명주기와 Spring ApplicationContext 사이에 위치한다.
  • Servlet container는 자체 표준에 따라 Filter instance 등록을 허용하지만, Spring-defined Bean은 인지할 수 없다.
    • Bean을 기준으로 필터링하는 Filter instance를 등록하려면 DelegatingFilterProxy를 등록해야 한다.
  • DelegatingFilterProxy는 Servlet container 표준 mechanism을 통해 등록할 수 있지만, 모든 filtering 작업은 Filter를 구현하는 Spring bean으로 위임(delegate)한다.

요약하면 bean을 기준으로 필터링 하려면 Filter를 구현체 Spring bean을 생성해야 하고, 이걸 FilterChain에 포함시키려면 DelegatingFilterProxy를 통해 등록해야 Servlet Container가 Bean filter를 Filter instance로 인지할 수 있다는 얘기.

DelegatingFilterProxyApplicationContext에서 Bean Filter0\text{Bean Filter}_0를 찾아보고, Bean Filter0\text{Bean Filter}_0를 호출한다.

public void doFilter(
        ServletRequest request,
        ServletResponse response,
        FilterChain chain
) {
	  Filter delegate = getFilterBean(someBeanName); 
	  delegate.doFilter(request, response); 
}
  • Filter를 lazy하게 얻는다.
    • Filter는 이미 Spring Bean으로 등록되어 있다.
  • 코드 상 delegateBean Filter0\text{Bean Filter}_0에 해당한다.

DelegatingFilterProxy를 쓰면

  • Filter bean instance를 찾는 것을 delay할 수 있다.
    • 이게 중요한 건 Spring container는 container 실행 전에 Filter를 등록해야 하기 때문
  • 그러나 Spring에서 Spring Bean을 load할 때는 ContextLoaderListener를 사용하는 게 전형적이고, ContextLoaderListener는 Filter bean 등록 전에 실행돼버린다.

FilterChainProxy

  • DelegatingFilterProxy는 하나의 Bean filter만 delegate할 수 있다.
  • FilterChainProxy는 다수의 Filter instance를 delegate할 수 있다.
    • 다수의 Filter instance는 SecurityFilterChain을 통해 연결한다.

SecurityFilterChain

  • FilterChainProxy에 쓰인다.

  • 다수의 Security Filter가 있을 때, 현재 요청에 대해 어느 Filter가 호출되어야 하는지 결정하는 데에 쓰인다. 여기서 처음으로 match되는 SecurityFilterChain 하나만 호출된다.
  • match의 기준은 URL이며, Ant style pattern이 쓰인다.
    사용 예시는 여기에 있다.
  • 예를 들어 /api/messages/가 요청되면 SecurityFilterChain0\text{SecurityFilterChain}_0SecurityFilterChainn\text{SecurityFilterChain}_n 모두 match되지만 먼저 match되는 SecurityFilterChain0\text{SecurityFilterChain}_0 하나만 호출된다.
  • SecurityFilterChain0\text{SecurityFilterChain}_0가 match되지 않으면 SecurityFilterChain1\text{SecurityFilterChain}_1을 보고,
    그것도 match되지 않으면 SecurityFilterChain2\text{SecurityFilterChain}_2를 보고, …
    SecurityFilterChainn\text{SecurityFilterChain}_n까지 반복한다.
  • 위 그림을 기준으로 항상 마지막에 SecurityFilterChainn\text{SecurityFilterChain}_n이 호출된다(/**는 ANY니까).
  • 여기서 주목할 것은 SecurityFilterChain0\text{SecurityFilterChain}_0가 3개의 security Filter instance로 구성되고, SecurityFilterChainn\text{SecurityFilterChain}_n은 4개의 security Filter instance로 구성된다는 것이다.
    이게 왜 중요하냐면 각 SecurityFilterChain은 unique하며 독립적으로 구성될 수 있다는 의미기 때문이다.
  • 사실 security Filter instance를 갖지 않는 SecurityFilterChain도 가능하다. 이건 Spring security가 그 요청을 무시하게 하기 위해 쓰인다.

SecurityFilter

  • FilterChainProxySecurityFilterChain을 통해 끼워진다.
  • SecurityFilter를 서로 다른 용도로 사용할 수 있다.
    • Authentication
    • Authorization
      • OAuth가 여기에 포함된다.
    • Exploit protection
  • Filter는 정해진 순서로 호출된다.
    • 인가는 인증보다 먼저 수행되어야 한다.
    • 순서가 어떻게 정해지는지 궁금하면 FilterOrderResgration을 보면 된다.
    • 불친절해서 죄송하지만 하다 보면 라이브러리를 직접 까보게 됩니다. 적어도 제 경우에는 그랬습니다.....
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults())
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults());
        return http.build();
    }

}

위와 같이 구성하면 Filter는 다음과 같은 순서로 구성된다.

  1. CsrfFilter Added by HttpSecurity#csrf
    • CSRF 공격에 대한 보호를 위해 호출한다.
  2. UsernamePasswordAuthenticationFilter Added by HttpSecurity#formLogin
  3. BasicAuthenticationFilter Added by HttpSecurity#httpBasic
    • 요청 인가를 위해 호출된다.
  4. AutorizationFilter Added by HttpSecurity#authorizeHttpRequests
    • 요청 인증을 위해 호출된다

실제로 내가 호출한 것과 다른 순서로 구성된다.
즉 내가 올바르게 호출하지 않아도 알잘딱깔센 해준다.

근데 호출 리스트에 안 올라갈 수도 있다.

그냥 출력해서 확인하면 됨

SecurityFilter 출력하기

2023-06-14T08:55:22.321-03:00  INFO 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]
  • 특정 요청에 대한 secutrity Filter 목록을 볼 수 있다.
  • 보통은 내가 만든 Filter가 호출되는지 확인하려고 쓴다.

Custom Filter 추가

public class TenantFilter implements Filter {

    @Override
    public void doFilter(
            ServletRequest servletRequest,
            ServletResponse servletResponse,
            FilterChain filterChain
    ) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String tenantId = request.getHeader("X-Tenant-Id"); 
        boolean hasAccess = isUserAllowed(tenantId); 
        if (hasAccess) {
            filterChain.doFilter(request, response); 
            return;
        }
        throw new AccessDeniedException("Access denied"); 
    }

}
  • Filter interface의 구현 클래스를 작성한다.
  • 근데 보통은 기본 제공되는 security filter 만으로도 충분하다고 한다.
  • JWT를 사용하는 경우 토큰으로 요청을 걸러내는 필터를 따로 만들어줘야 한다. 즉 토큰 인증을 위한 custom filter를 추가해야 하는데, 여기서 그 설명을 확인할 수 있다.

Sample code에서 하는 작업

  1. Request header에서 tenant ID를 뽑아낸다.
  2. 현재 유저가 tenant ID에 접근 권한을 갖고 있는지 확인한다.
    1. 갖고 있다면
      Filter chain 내 나머지 filter를 호출한다.
    2. 갖고 있지 않다면
      AccessDeniedException을 throw한다.

Filter 구현하기 싫으면

  • OncePerRequestFilter와 같은 필터 클래스를 상속받으면 된다.
    • 근데이제 filter가 되기 위한 base class인
    • Request 당 한 번만 호출된다(닉값).
    • doFilterInternal 메소드가 제공된다.
      • parameter는 HttpServletRequest, HttpServletResponse.
    • doFilter는 필터가 동작하게 하는 메소드고, doFilterInternaldoFilter가 호출됐을 때 수행할 내용을 정의하는 메소드다.
profile
Java를 하고 싶었지만 JavaScript를 하게 된 사람

0개의 댓글

관련 채용 정보