Spring Framework/Spring Security

Spring Security 필터 아키텍처 이해하기 - Spring Security Hands On 1

Junuuu 2023. 10. 7. 00:01

개요

Spring Security를 사용하며 SecurityConfig 설정을 수행하다 왜 인증이 필요한 Path에만 필터가 제공되지 않는 것일까?

라는 의문을 시작으로 아키텍처부터 파악해서 Spring Security와 친해지려고 합니다.

 

버전은 Spring Boot 3.1 기준입니다.

 

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
open class SecurityConfig {
    @Bean
    fun filterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
        httpSecurity
            .csrf {
                it.disable()
            }
            .authorizeHttpRequests {
                it
                    .requestMatchers("/**").permitAll()
                    .requestMatchers("/admin/**").authenticated()
            }.addFilterBefore(AuthorizationFilter(), BasicAuthenticationFilter::class.java)
        return httpSecurity.build()
    }
}

/**로 모든 경로를 허용했으며 /admin/** 에는 인증이 필요하도록 구성하였습니다.

이후 AuthorizationFilter를 OncePerRequestFilter를 상속받아 구현하였으나 어떤 경로로 접근하던 해당 필터가 동작하며 401 예외가 발생하게 되었습니다.

 

내부 구조를 모르다 보니 파악하는데 어려웠고 공식문서를 기반으로 아키텍처를 이해해보려 합니다.

 

Servlet FilterChain

Figure 1. FilterChain

Security도 결국 Servlet 기반의 FilterChain내부에서 돌아갑니다.

 

FilterChainProxy

Spring Security를 사용하게 되면 FilterChainProxy를 사용하게 되며 우리가 등록하는 SecurityFilterChain 빈을 등록합니다.

 

Multiple SecurityFilterChain

 

SecurityFilterChain은 여러 개를 사용할 수 있습니다.

이중 RequestURL의 패턴이 가장 먼저 일치하는 것만 호출됩니다.

N개의 SecurityFilterChain을 두고 다양하게 인증 서비스를 구현할 수 있습니다.

 

SecurityFilterChain내부에 커스텀하게 필터를 채워 넣을 수 있습니다.

그림을 보면 위에는 3개, 아래는 4개입니다.

 

다시 보는 코드

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
open class SecurityConfig {
    @Bean
    fun filterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
        httpSecurity
            .csrf {
                it.disable()
            }
            .authorizeHttpRequests {
                it
                    .requestMatchers("/**").permitAll()
                    .requestMatchers("/admin/**").authenticated()
            }.addFilterBefore(AuthorizationFilter(), BasicAuthenticationFilter::class.java)
        return httpSecurity.build()
    }
}

필터를 등록해 주었으나, permitAll경로와는 상관없이 SecurityFilterChain에 등록되어 있고, Servlet 필터가 호출된 후 SecurityFilterChain의 필터들이 순차적으로 호출이 되는 아키텍처입니다.

따라서 인증이 걸려있는 것과는 전혀 상관없이 필터가 호출되는 것이 정상적입니다.

 

그러면 어떻게 대응할 수 있을까?

이를 해결하기 위해 해당 Docs를 참고하여 WebSecurityCustomizer를 사용해 보았습니다.

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

 

Spring Security without the WebSecurityConfigurerAdapter

In Spring Security 5.7.0-M2 we deprecated the WebSecurityConfigurerAdapter, as we encourage users to move towards a component-based security configuration. To assist with the transition to this new style of configuration, we have compiled a list of common

spring.io

 

하지만 사용해 보면 WARN 로그가 발생합니다.

Github Issue에 정리되어 있으며 해결방안도 제시되고 있습니다.

 

https://github.com/spring-projects/spring-security/issues/10938

 

WARN when ignoring antMatchers - please use permitAll · Issue #10938 · spring-projects/spring-security

When I use web.ignoring().antMatchers() I'd like to see a DEBUG message instead of a WARNING for each ignored pattern. I'm confused by the message saying it's not recommended and I should use permi...

github.com

 

SecurityFilterChain을 2개로 분리하자

아키텍처에 소개되었듯이 Path에 해당하는 FilterChain을 2개로 분리하는 방법을 사용할 수 있습니다.

 

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
open class SecurityConfig {
    @Bean
    @Order(1)
    fun filterChain(httpSecurity: HttpSecurity): SecurityFilterChain {
        httpSecurity
            .csrf {
                it.disable()
            }
            .authorizeHttpRequests {
                it.anyRequest().authenticated()
            }.addFilterBefore(MyAuthorizationFilter(), UsernamePasswordAuthenticationFilter::class.java)
        return httpSecurity.build()
    }

    @Bean
    @Order(0)
    fun exceptionSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .securityMatchers {
                it.requestMatchers("/actuator/**")
                    .requestMatchers("/swagger/**")
            }
            .requestCache {
                it.disable()
            }
            .securityContext {
                it.disable()
            }
            .sessionManagement {
                it.disable()
            }
        return http.build()
    }

}

actuator와 swagger의 경로에 해당한다면 커스텀하게 구현한 인증필터가 없는 SecurityFilterChain이 호출됩니다.

하지만 그렇지 않은 경우에는 MyAuthorizationFilter가 포함된 SecurityFilterChain이 호출되고 인증필터가 호출됩니다.

 

resources(css, js 등)의 경우 securityContext 등에 대한 조회가 불필요하므로 requestCache, securityContext, sessionManagement를 disable 합니다.

 

DefaultSecurityFilterChain 디버깅

2개의 클래스에 디버깅을 걸어보면 URI에 따라 패턴이 일치하는 mathces의 결과와 getFilter를 통해 가져오는 필터의 개수를 살펴볼 수 있습니다.

 

Order(0)에 걸리게 되면 7개의 필터를 가져오고 그렇지 않으면 10개의 필터를 가져옵니다.

 

 

참고자료

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

 

Architecture :: Spring Security

The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec

docs.spring.io