개발 일기

[Spring Security] 스프링 시큐리티 - Filter, DelegatingFilterProxy, FilterChainProxy, SecurityFilterChain 본문

Back-End/Spring

[Spring Security] 스프링 시큐리티 - Filter, DelegatingFilterProxy, FilterChainProxy, SecurityFilterChain

개발 일기장 주인 2024. 5. 2. 19:42

기존에는 Express로만 개발을 해봤기 때문에 인증 인가를 도와주는 프레임워크라는 것이 따로 없었다.

그러나 이번에 스프링을 공부를 해보니 인증 인가를 도와주는 스프링 시큐리티라는 프레임워크를 제공했다.

그래서 스프링 시큐리티가 무엇인지, 어떻게 동작하는건지에 대해 이해 후 적용해보기 위해 정리하게 됏다.

Spring Security

Spring 공식 문서를 보면 다음과 같이 쓰여져 있다.

"Spring Security is a framework that provides authentication, authorization, and protection against common attacks.."


즉, 인증, 권한 부여, 그리고 일반적인 공격으로부터의 보호를 제공하는 프레임워크라고 한다.
이는 Filter를 통해서 이러한 과정들을 처리한다. 해당 프레임워크는 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.

  • Spring Security는 Filter 기반으로 동작한다. 즉, 위 그림에서 보이는 것과 같이 Spring MVC와 완전히 분리되어 있다.
  • Spring Security는 Bean으로 등록될 수 있다. 이를 통해 Spring의 IoC 컨테이너에 등록하여 사용할 수 있다는 것이다.
    나는 분명 Filter 기반으로 동작하는 Spring Security이기 때문에 Spring MVC와는 분리되어 있다고 했었는데 알고 보니 Spring Web MVC로 들어가기전에 WAS의 Filter에서 요청을 가로챈다. 그런 다음 스프링 단(스프링 컨테이너)으로 들어가서 스프링 시큐리티 필터를 거치면서 로직이 수행되고 다시 WAS단으로 넘어와서 그 다음 필터로 넘어가게 된다.

Spring Security Architecture 

Servlet Filter

Spring Security 공식 문서를 참고하여 아키텍처에 대해 조금 더 구체적으로 뜯어보자. 서블릿 기반 애플리케이션 내 Spring Security의 고급 아키텍처에 대해 설명한다고 되어 있다.

위에서 언급했듯이 Spring Security는 Servlet Filter를 기반으로 동작하기 때문에 필터를 이해해야한다. 그러나 이전 게시글에서 이미 정리했었으나 다시 이 공식 문서를 기반으로 정리해보겠다.

클라이언트가 어플리케이션에 요청을 보내면 Servlet Container에서 요청 URI를 바탕으로 HttpServletRequest를 처리하는 Filter 인스턴스와 Servlet을 가지고 있는 FilterChain을 생성한다. Servlet은 하나의 HttpServletRequest와 HttpServletResponse를 처리할 수 있었다. 그러나 위 그림과 같이 여러 개의 Filter가 사용될 수 있다.

Filter의 주요 기능은 다음과 같습니다:

  1. 하향 Filter 인스턴스나 Servlet의 호출 방지: 이 경우 Filter는 일반적으로 HttpServletResponse를 작성한다.
  2. 하향 Filter 인스턴스와 Servlet에서 사용하는 HttpServletRequest나 HttpServletResponse 수정: Filter는 FilterChain으로 전달되는 HttpServletRequest나 HttpServletResponse를 수정할 수 있다.

Filter의 강력한 기능은 FilterChain에서 전달되는 것에서 나온다. 그래서 Filter의 순서가 중요하다.

 

 

 

 

 

Spring Security를 예시로 들어보자

DelegatingFilterProxy 

클라이언트의 요청이 어플리케이션으로 들어오게되면 첫번째 WAS에서 필터들을 거치게된다. 이때 Spring Security의 의존성이 활성화되어 있다면 이 DelegatingFilterProxy에 의해서 해당 요청이 가로채어지고 Spring Container의 FilterChainProxy로 해당 요청이 들어오게 된다.


Spring Security는 DelegatingFilterProxy를 이용하여 보안 필터 체인(Security Filter Chain)을 구성한다. 이 필터 체인은 서블릿 컨테이너에 등록되어 모든 HTTP 요청을 가로채고 보안 관련 작업을 수행한다.
DelegatingFilterProxy는 서블릿 컨테이너에 등록되지만, 실제 Security 관련 작업은 Spring Container에 빈으로 등록된 FilterChainProxy에게 요청하고 그 안에 정의된 SecurityFilterChain이 처리다. 즉, DelegatingFilterProxy는 이들 Filter들과의 연결을 담당하는 것이다. 이때 SecurityFilterChain은 SecurityConfig.class에서 정의되고 인증, 인가, CSRF 보호 등 여러 보안 관련 필터들을 거치며 작업을 수행한다.

따라서 Spring Security를 사용하면 서블릿 컨테이너는 DelegatingFilterProxy만을 알고 있고, 실제 Security 작업은 Spring Container의 FilterChainProxy에서 정의된 Filter들에게 위임된다.

SecurityFilterAutoConfiguration.class에서

아래와 같은 생성자를 통해 "springSecurityFilterChain"이라는 이름의 DelegatingFilterProxy가 생성되고 서블릿 컨테이너에서 관리된다.
그런 다음 아래와 같이 DelegatingFilterProxy.class에서 this.invokeDelegate(delegateToUse, request, response, filterChain);가 호출되면 springSecurityFilterChain이라는 이름을 가진 FilterChain 스프링 빈이 Spring Ioc Container에 등록된다.


이것은 DelegatingFilterProxy.class의 this.invokeDelegate() 메소드에 브레이킹 포인트를 놓고 실행시켜 localhost:8080으로 요청을 보낸 후 디버거를 확인해보면 직접 확인해볼 수 있었다.
아래 beanName = "springSecurityFilterChain"이라고 되어있는데 이를 통해 FilterChain이 스프링 빈으로 스프링 컨테이너에 등록된 것을 확인할 수 있었다.

 

 

FilterChainProxy: "springSecurityFilterChain"

위 내용과 같이  DelegatingFilterProxy가 서블릿 컨테이너에 등록된 상태에서 요청이 들어오면 DelegatingFilterProxy가 해당되는 요청이 들어오게 되면 가로채고 DelegatingFilterProxy 내부 메소드에 의해 springSecurityFilterChain이라는 이름의 FilterChainProxy가 스프링 빈으로 스프링 컨테이너에 등록된다. 그리고 아래에서도 보겠지만 앞으로 우리가 정의해줄 SecurityFilterChain들을 리스트업해서 이 FilterChainProxy에서 들고 있는다. 즉, SecurityFilterChain은 여러개를 선언할 수 있다.

List<SecurityFilterChain> 필드

이들은 SecurityFilterChain들을 위와 같이 리스트업해서 들고 있고 요청에 알맞은 SecurityFilterChain을 매핑해준다.

 

FilterChainProxy.class에서 getFilters()를 디버깅해보면 왼쪽과 같이 결과가 나온다. 추후에 내가 저러한 필터들을 지정해줄 수 있고 그러면 size가 커질 것이다. 이러한 것들을 모두 리스트업 해서 가지고 있는 것이다.

 

 

 

 

 

 

 

SecurityFilterChain을 2개 등록하고 다시 디버깅해보면?

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
        http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/user").permitAll());

        return http.build();
    }

    @Bean
    SecurityFilterChain securityFilterChain2(HttpSecurity http) throws Exception{

	    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/admin").permitAll());

		return http.build();
    }
}

다음과 같이 size = 2가 된다.

 

멀티 SecurityFilterChain 경로 설정

그러나 이때 문제점이 있다.

FilterChainProxy는 N개의 SecurityFilterChain 중 하나를 선택해서 요청을 전달하는데 그 기준은

  1. 등록된 인덱스 순
  2. FilterChain에 대한 RequestMatcher 값이 일치하는지 확인

이 두가지이다. 그래서 이렇게 여러개의 SecurityFilterChain이 선언된다면 경로 설정을 해주는 작업이 필요하다. 안해주게 되면 무조건 첫번째 SecurityFilterChain으로만 통과하기 때문에 그 이후의 SecurityFilterChain은 선언해준 의미가 없기 때문이다.

예를 들어 위의 코드를 예시로 "/user"과 "/admin" 각각에 요청을 보내서 권한이 있으면 각각 "user", "admin"반환하는 컨트롤러가 있다고 치자.  http://localhost:8080/user으로 요청을 보내게 되면 1번 기준에 의해 등록된 인덱스에서 우선 순위이기 때문에 "user"라는 문자열이 올바르게 반환된다. http://localhost:8080/admin라고 요청을 보내게 되면 securityFilterChain2를 통과하게 된다면 권한이 생기지만 위의 기준에 의해 2번이 아닌 1번이 실행되게 된다. 따라서 권한을 얻지 못하고 "admin"이라는 문자열을 반환하는데 실패하고 403 Forbidden을 반환하게 된다.

멀티 SecurityFilterChain 경로 설정

그래서

@Bean
public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception {

    http
            .securityMatchers((auth) -> auth.requestMatchers("/user"));

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/user").permitAll());

    return http.build();
}

@Bean
public SecurityFilterChain filterChain2(HttpSecurity http) throws Exception {

    http
            .securityMatchers((auth) -> auth.requestMatchers("/admin"));

    http
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/admin").authenticated());

    return http.build();
}

다음과 같이 해놓으면 url에 맞게 FilterChain을 거치게 된다. 이렇게 인가를 위한 매핑에 대한 작업이 필요한다.

또한 @Bean아래에 @Order(1), @Order(2) 이런식으로 순서를 지정해줄수도 있다.

 

SecurityFilterChain을 거치게 된다면 내부적으로 여러 가지 필터를 거치게 되는데

이때 서버의 리소스를 소비하여 낭비가 발생하기 때문에 특정 요청은 필터를 통과하지 못하도록 설정할 수 있다.

보통 정적 자원 (이미지, CSS)의 경우 필터를 통과하지 않도록 아래 구문을 통해 설정할 수 있다.

설정시 하나의 SecurityFilterChain이 0 번 인덱스로 설정되며 해당 필터 체인 내부에는 필터가 없는 상태로 생성된다.

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {

    return web -> web.ignoring().requestMatchers("/img/**");
}

 

 

 

 

 

 

 

아래 그림은 Spring Security의 인증 관련 아키텍처를 도식화한 것이다.

  1. HTTP 요청 수신: 사용자가 로그인 정보와 함께 HTTP 인증 요청을 보낸다
  2. 유저 정보 기반 인증 토큰 생성: AuthenticationFilter가 요청을 가로채고, 사용자가 제공한 정보를 기반으로 UsernamePasswordAuthenticationToken이라는 인증용 객체를 생성한다.
  3. Filter를 통한 AuthenticationToken 전달: 생성된 AuthenticationToken은 Filter를 통해 AuthenticationManager로 전달된다.
  4. AuthenticationProvider 목록으로 인증 시도: AuthenticationManager는 등록된 AuthenticationProvider들을 조회하며 인증을 시도한다.
  5. UserDetailsService의 요구: 인증 시도 중에 실제 사용자 인증 정보를 가져오는 UserDetailsService에 사용자 정보를 전달한다.
  6. UserDetails를 이용한 사용자 정보 조회: UserDetailsService는 전달받은 사용자 정보를 기반으로 데이터베이스에서 사용자 정보를 조회하여 UserDetails 객체를 생성한다.
  7. UserDetails를 통한 사용자 정보 비교: AuthenticationProvider들은 전달받은 UserDetails를 사용하여 사용자 정보를 비교하여 인증을 시도한다.
  8. 인증 객체 반환 또는 AuthenticationException: 인증이 완료되면 사용자의 권한 등을 포함한 Authentication 객체를 반환하거나, 인증에 실패하면 AuthenticationException을 발생시킨다.
  9. 인증 완료: 최초의 AuthenticationFilter에 인증된 Authentication 객체가 반환된다.
  10. SecurityContext에 인증 객체 설정: 반환된 Authentication 객체는 SecurityContext에 저장되어 현재 사용자의 인증 상태를 유지한다.

Filter, DelegatingFilterProxy, FilterChainProxy, SecurityFilterChain