웹 개발/Spring

[스프링 시큐리티] 스프링 시큐리티의 구조를 살펴보자!

어썸오184 2021. 11. 23. 04:46
728x90
반응형

스프링 시큐리티의 구조

스프링 시큐리티는 웹 애플리케이션의 인증과 인가 및 그 외 일반적인 웹 공격에 대한 방어를 제공하는 스프링의 하위 프레임워크이다.

  • 인증: 내 신원을 확인하는 과정
  • 인가: 내가 특정 리소스에 접근할 권한이 있는지 확인하는 과정

스프링 시큐리티는 주로 서블릿 필터와 이들로 구성된 필터체인을 통해 웹 요청에 대한 보안 관련 처리를 수행한다. 서블릿 필터란 HTTP 요청을 가로채 전처리 및 후처리를 수행할 수 있도록 만들어진 자바 표준 기술이다.

필터는 체인으로 구성될 수 있으며, 하나의 필터가 자신의 역할을 한 후 요청과 응답 객체를 다음 필터로 넘길 수 있다.

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
}

서블릿 필터는 논리적으로 서블릿 컨테이너와 서블릿 사이에 위치한다. 스프링 웹 MVC 애플리케이션이라면 이 서블릿이 바로 DispatcherServlet의 인스턴스이다.

즉 스프링 시큐리티가 하는 일을 아주 간단하게 말하자면 클라이언트의 요청이 서블릿에 도착하기 전에 여러 필터를 거치게해서 인증, 인가 및 여러 보안처리를 하는 것이다.

image

DelegatingFilterProxy

자 그럼 스프링 시큐리티는 어떻게 웹 요청을 가로채서 인증, 인가 처리 등 다양한 동작을 수행할 수 있는 것일까?

스프링 시큐리티의 구조를 이해하기 위해 가장 먼저 알아야할 것이 바로 DelegatingFilterProxy 라는 필터 구현체이다. DelegatingFilterProxy는 이름 그대로 필터 체인을 통해 전달된 웹 요청을 스프링에게 "위임"하는 역할을 한다. DelegatingFilterProxy는 스프링 프레임워크에서 제공(org.springframework.web.filter.DelegatingFilterProxy)하는 빈으로 스프링 시큐리티에 종속된 기술이 아님을 주의하자.

일반적으로 서블릿 컨테이너 자체에 필터를 등록하는 것도 가능하지만 그렇게 할 경우 필터가 스프링 컨테이너에 등록된 빈들을 인식하지 못한다. 그렇기 때문에 스프링은 서블릿 컨테이너와 스프링의 ApplicationContext 를 이어주는 다리 역할로 DelegatingFilterProxy를 이용한다.

DelegatingFilterProxy는 단순히 자신에게 넘어온 체인을 모두 Filter 인터페이스를 구현한 스프링 빈들에게 위임하는(delegate) 역할을 한다.

DelegatingFilterProxy가 하는 일을 의사코드로 표현하면 다음과 같다. Filter를 구현한 빈에게 요청, 응답 객체를 넘기기만 한다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    // Lazily get Filter that was registered as a Spring Bean
    // For the example in DelegatingFilterProxy 
delegate
    is an instance of Bean Filter0
    Filter delegate = getFilterBean(someBeanName);
    // delegate work to the Spring Bean
    delegate.doFilter(request, response);
}

DelegatingFilterProxy가 하는 다른 중요한 역할 하나는 Filter를 구현한 빈 인스턴스들을 조회하는 시기를 늦춘다는 것이다. 보통 Filter 인스턴스는 컨테이너가 뜨기 전에 필요한데 DelegatingFilterProxy는 Lazily하게 필터를 얻어오기 때문에 문제 없이 필터가 동작할 수 있다.

FilterChainProxy와 SecurityFilterChain

FilterChainProxy스프링 시큐리티가 제공하는 특별한 Filter 구현체이다. 앞서 언급했듯이 DelegatingFilterProxy는 단순히 웹 요청을 필터 빈에게 위임할 뿐이다. 때문에 실제로 웹 요청을 처리할 Target Filter Bean을 설정해야 하는데, 이때 스프링 시큐리티의 FilterChainProxy가 해당 빈으로 설정된다.

SecurityFilterAutoConfiguration에서 DelegatingFIlterProxyRegistrationBean을 통해 DelegatingFilterProxy 인스턴스를 생성한다.

org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
...
@Bean
@ConditionalOnBean(
    name = {"springSecurityFilterChain"}
)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
    SecurityProperties securityProperties) {
    
    DelegatingFilterProxyRegistrationBean registration = 
            new DelegatingFilterProxyRegistrationBean("springSecurityFilterChain", new ServletRegistrationBean[0]);
    registration.setOrder(securityProperties.getFilter().getOrder());
    registration.setDispatcherTypes(this.getDispatcherTypes(securityProperties));
    return registration;
}
...

 

DelegatingFilterProxy는 FilterChainProxy로 요청 처리를 위임한다. FilterChainProxy는 아래 그림처럼 시큐리티 필터 목록을 가진 SecurityFilterChain을 호출한다. 이 필터 체인을 구성하는 Security Filter들이 바로 각종 인가, 인증 등 다양한 기능을 제공하는 스프링 시큐리티의 핵심이다.

SecurityFilterChain 안에 들어있는 시큐리티 필터들은 (대부분) 빈이다. 이 빈들은 모두 FilterChainProxy에 의해 등록된다. 왜 시큐리티의 필터들은 서블릿 컨테이너나 DelegatingFilterProxy에 의해 직접 등록되지 않고, FilterChainProxy를 거쳐서 등록될까? FilterChainProxy는 몇 가지 장점들을 제공한다.

첫번째로 스프링 시큐리티 서블릿 지원의 시작점을 제공한다. 따라서 스프링 시큐리티와 관련해서 트러블슈팅이 필요하다면 FilterChainProxy를 디버깅의 시작점으로 잡을 수 있다

두 번째로 FilterChainProxy는 메모리 누수 방지를 위해 SecurityContext를 비우는 등 스프링 시큐리티 사용에 있어 중심부 역할을 수행한다. 또 스프링 시큐리티의 HttpFirewall을 적용하여 특정 타입의 공격으로부터 애플리케이션을 보호한다.

또한 FilterChainProxy는 SecurityFilterChain이 언제 호출될지를 결정함으로써 유연성을 제공한다.

일반적인 서블릿 컨테이너의 Filter는 URL만을 기준으로 호출된다. 그러나 FilterChainProxy를 활용하면 RequestMatcher 인터페이스를 통해 HttpServletRequest를 기반으로 이루어지는 모든 호출을 결정할 수 있다.

이와 관련한 설정들은 WebSecurityConfigurerAdapter를 상속한 설정 클래스의 configure 메서드를 오버라이드하여 설정할 수 있다. 예를 들어 아래와 같은 빈을 등록하여 자동 로그인 설정이나 로그아웃 설정 등 스프링 필터를 이용하는 모든 기능들에 대한 설정을 할 수 있다.


import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
...

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) {
        // 아래의 URL에 매칭되는 요청에 대해서는 필터 처리를 하지 않음
        web.ignoring().antMatchers("/assets/**");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 사용자 계정 등록
        auth.inMemoryAuthentication()
            .withUser("user").password("{noop}user123").roles("USER")
            .and()
            .withUser("admin").password("{noop}admin123").roles("ADMIN")
        ;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/mypage").hasAnyRole("USER", "ADMIN")
                .anyRequest().permitAll()
                .and()
            // 자동 로그인 설정
            .rememberMe()
                .rememberMeParameter("remember-me")
                .tokenValiditySeconds(300)
                .and()
            // 로그아웃 설정
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                .and()
            // HTTP 요청을 HTTPS 요청으로 리다이렉트
            .requiresChannel()
                .anyRequest().requiresSecure()
                .and()
            /**
             * 예외처리 핸들러
             */
            .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler())
        ;
    }

    ...
}

 

여러 개의 SecurityFilterChain을 사용할 때, FilterChainProxy는 어떤 SecurityFilterChain이 사용되어야 하는지 결정할 수 있다. 예를 들어 아래의 그림처럼 설정을 하게되면, "/api/messages" 라는 URL이 호출될 때는 0번 SecurityFilterChain만 호출한다. 그리고 다른 모든 요청에 대해서는 n번 SecurityFilterChain이 호출된다. 각각의 SecurityFilterChain은 독립적이며 고립적으로 설정되어있다. 특정한 요청에 대해서는 아얘 어떠한 필터도 거치지 않도록 설정할 수도 있다(위 예시 코드에서는 /assets/** 에 대해 아무런 필터 처리도 하지 않는다).

Security Filters

스프링 시큐리티에는 수많은 필터들이 구현되어 있다. 스프링 시큐리티를 잘 사용한다는 것은 결국 이 필터들을 잘 활용한다는 것을 의미한다. 이 곳에서 스프링 시큐리티에 존재하는 필터들의 목록을 확인할 수 있다. 해당 페이지에 나열된 필터들은 무작위로 나열된 것이 아니라 웹 요청이 통과하는 순서대로 나열되어 있다. 즉 가장 위에 있는 ChannelProcessingFilter가 가장 먼저 동작한다.

몇 가지 주요한 Security Filter들을 살펴보면 아래와 같다

  1. ChannelProcessingFilter: 웹 요청이 어떤 프로토콜(HTTP 또는 HTTPS)로 전달되어야 하는지 처리
  2. SecurityContextPersistenceFilter: SecurityContextRepository를 통해 SecurityContext를 Load/Save 처리
  3. LogoutFilter: 로그아웃 URL로 요청을 감시하여 매칭되는 요청이 있으면 해당 사용자를 로그아웃 시킴
  4. UsernamePasswordAuthenticationFilter: ID/비밀번호 기반 Form 인증 요청 URL(기본값: /login) 을 감시하여 사용자를 인증함
  5. DefaultLoginPageGeneratingFilter: 로그인을 수행하는데 필요한 HTML을 생성함
  6. RequestCacheAwareFilter: 로그인 성공 이후 인증 요청에 의해 가로채어진 사용자의 원래 요청으로 이동하기 위해 사용됨
  7. SecurityContextHolderAwareRequestFilter: 서블릿 3 API 지원을 위해 HttpServletRequest를 HttpServletRequestWrapper 하위 클래스로 감쌈
  8. RememberMeAuthenticationFilter: 요청의 일부로 remeber-me 쿠키 제공 여부를 확인하고, 쿠키가 있으면 사용자 인증을 시도함
  9. AnonymousAuthenticationFilter: 해당 인증 필터에 도달할때까지 사용자가 아직 인증되지 않았다면, 익명 사용자로 처리하도록 함
  10. ExceptionTranslationFilter: 요청을 처리하는 도중 발생할 수 있는 예외에 대한 라우팅과 위임을 처리함
  11. FilterSecurityInterceptor: 접근 권한 확인을 위해 요청을 AccessDecisionManager로 위임

다음 글에서는 스프링 시큐리티의 인증 처리 구조를 살펴보기로 한다. 인증 처리 또한 마친가지로 Security Filter에 의해 이루어진다. 위에서 소개한 필터 목록 중 4번 UsernamePasswordAuthenticationFilter 가 여러 인증 처리를 수행하는 필터들 중 하나로 아이디와 패스워드를 기반으로 인증 처리를 한다.

728x90
반응형