Spring Security Architecture

Kyojun Jin·2024년 5월 13일

Spring

목록 보기
3/12

이 문서는 서블렛 기반 어플리케이션의 고수준 아키텍쳐에 대해 설명한다. 스프링 시큐리티의 Authentication, Authorization과 Protection Against Exploits 은 이 고수준 아키텍쳐로 구현되었다. (이걸 이해해야 스프링 시큐리티의 각 기능을 이해할 수 있다.)

FilterChain

스프링 시큐리티는 클라이언트의 요청을 서블렛(서버)으로 보내기 전에 그 요청을 거르고 거르고 거르고 거르고... 거르는 방식으로 동작한다.

거른다는 동작을 하는 모듈은 Filter라고 하며, 여기서 거른다는 것은 다음의 동작을 일컫는다.

  • 다음 Filter나 서블렛으로 가는 것을 방지한다. 이때 HttpServeletResponse는 Filter가 작성한다.
  • 다음 Filter나 서블렛이 사용할 HttpServletRequest 이나 HttpServeletResponse 을 수정한다.

위의 동작들을 하는 Filter들을 묶어서 FilterChain이라고 한다.

이때 거르는 전략은 유저마다, 로그인 시간 (세션이나 토큰 만료), URL마다 천차만별로 달라진다. 이런 변화무쌍한 전략을 구현하기 위하여 Spring Security는 메카니즘 (사용자의 요청을 거르는 큰 흐름)과 정책(범위와 권한을 때에 따라 다르게 적용하는 법)을 분리한다. 여기서 이 메카니즘은 DelegatingFilterProxy 내에서 구현하며, 정책들은 SecurityFilter에서 구현한다.

DelegatingFilterProxy

실제 웹에서 FilterChain은 사용자의 동작마다 수많은 종류가 나올 수 있다. 이 모든 Chain을 한 번에 동작시키기 위한 DelegatingFilterProxy를 제공한다. 이는 Filter의 동작들을 Delegate(위임)하는 Proxy(대리자)이다. 즉, 이는 Filter가 하는 모든 일을 누군가에게 위임하는 객체이다. 서블렛은 각기 다른 Filter 인스턴스들을 등록할 수 있지만 스프링에 등록된 빈들(실제 필터링 객체)에 대해선 알지 못한다. 그래서 기본적인 서블렛 컨테이너 메카니즘에 DelegatingFilterProxy 를 등록하여서 Filter를 구현하는 스프링 Bean들이 하는 모든 일들을 위임시킬 수 있다.

그림에는 DelegatingFilterProxy 내부에 Bean Filter_0 가 있다. 상황에 맞는 Bean Filter_0 를 어딘가에서 가져와서 그에게 필터링 작업을 위임하는 일을 한다. 그 어딘가는 SecurityFilterChain이며, 상황에 맞는 것을 고르는 것은 FilterChainProxy이다.

FilterChainProxy

FilterChainProxySecurityFilterChain 안에 있는 다수의 Filter 인스턴스들에게 보안 작업을 위임한다. FilterChainProxy 또한 Bean이라서, DelegatingFilterProxy가 가지고 있다.

SecurityFilterChain

SecurityFilterChain은 여러가지의 SecurityFilter의 집합이다. FilterChainProxy가 현재 요청을 거를 때 어떤 Filter 인스턴스를 쓸 지 정할 때 이 SecurityFilterChain이 사용된다.

아키텍쳐를 원문 그대로 이해한다면 이쯤에서 상당히 헷갈릴 수가 있다. 왜냐면 Filter라는 단어가 '거르는 주체'와 '거름망 종류'의 두 가지 뜻으로 혼용되기 때문이다.

DelegatingFilterProxy는 거름망을 사용하는 주체이며, SecurityFilter가 진짜 거름망이다.

DelegatingFilterProxy 는 FilterChainProxy를 사용해서 상황에 맞는 적절한 SecurityFilterChain을 고른다.

SecurityFilterChain은 Security Filter들로 사용자의 요청 (=HttpServletRequest)를 거른다.

SecurityFilterChain은 Bean으로써 FilterChainProxy 가 등록한다.

DelegatingFilterProxy 와 SecurityFilterChain(=SecurityFilter들의 집합) 들 사이에 FilterChainProxy를 둔 이유는 다음과 같다.

  1. 서블렛에서 스프링 시큐리티가 쓰이는 부분을 디버깅하기 쉽다. FilterChainProxy부터 시작하면 되기 때문.
  2. FilterChainProxy 는 필요한 일만 처리한다. 이 객체는 SecurityContext 내부를 완전히 지워서 메모리 누수를 막는다. 특정 유형의 공격에서 앱을 보호하기 위해 HttpFirewall을 사용할 때도 이 방법이 적용된다.
  3. 어떤 SecurityFilterChain 이 참조될 지를 정할때 유연성을 제공한다. 서블렛 컨테이너에선 Filter는 URL 하나만으로 특정된다. FilterChainProxy가 RequestMatcher 인터페이스를 사용해서 HttpServletRequest를 통해 이를 정할 수 있다.

(SecurityContext: 이 모든 Filter 작업이 완료됐을 때 사용자 정보, 암호, 권한을 담아두는 객체)

이 그림은 FilterChainProxy 가 어떤 SecurityFilterChain이 사용될 지 고르는 것을 보여준다. URL과 매칭되는 첫 SecurityFilterChain이 선택된다. 만약에 /api/messages/가 요청된다면 /api/** 패턴을 가진 SecurityFilterChain_0 이 선택될 것이다. SecurityFilterChain_n이 /** 로 매칭되더라도, 처음이 아니기 때문에 매칭되지 않는다.

만약에 그냥 /messages/라는 요청이 들어왔다면 SecurityFilterChain_0 의 /api/** 와 매칭되지 않기 때문에 FilterChainProxy 는 계속해서 다른 SecurityFilterChain들을 시도해본다. 맞는 SecurityFilterChain가 아무것도 없다면, SecurityFilterChain_n이 참조된다.

Security Filters

위의 과정을 통해 Security Filters 는 SecurityFilterChain API와 같이 FilterChainProxy 에 삽입된다. SecuriyFilter는 인증, 권한 부여, 취약점 보호 등의 다양한 일을 한다.

Kotlin

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            csrf { }
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            httpBasic { }
            formLogin { }
        }
        return http.build()
    }

}

Java

@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();
    }

}

위는 SecurityFilterChain의 예시이다. 여기서 저 한줄 한줄이 Filter가 되는 것이다.

CsrfFilter, UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter, ...

0개의 댓글