개발일기/Spring

Spring security Architecture

ignuy 2024. 11. 12.

애플리케이션 개발자가 모든 보안 및 인증 관련 사항을 하나하나 구현하고 신경을 쓰기엔 현실적으로 시간과 자원이 많이 든다. 이에 스프링은 하위 라이브러리로 스프링 기반 애플리케이션의 보안(인증, 권한, 인가)을 담당하는 Spring security를 개발하여 애플리케이션 개발자들에게 편의를 제공한다.

다만, 보안에 관련된 개발 특성상 코드 변화가 잦고 버전마다 차이가 있을 수 있으므로 레퍼런스는 공식 docs에서 얻는것이 가장 확실한 방법이다.

아래 블로그 본문은 Spring Security의 6.3.4 버전 공식 docs를 필자의 생각과 함께 재구성하였다.

Spring Security “사용”하기

Security를 “사용”만 하는 방법은 정말 간단하다.

@EnableWebSecurity
@Configuration
class DefaultSecurityConfig {

    @Bean
    @ConditionalOnMissingBean(UserDetailsService::class)
    fun inMemoryUserDetailsManager(): InMemoryUserDetailsManager {
        val generatedPassword = // ... 
        return InMemoryUserDetailsManager(
            User.withUsername("user")
                .password(generatedPassword)
                .roles("USER")
                .build()
        )
    }

    @Bean
    @ConditionalOnMissingBean(AuthenticationEventPublisher::class)
    fun defaultAuthenticationEventPublisher(delegate: ApplicationEventPublisher): DefaultAuthenticationEventPublisher {
        return DefaultAuthenticationEventPublisher(delegate)
    }
}
  1. Security를 위한 Configuration 빈에 @EnableWebSecurity 애노테이션을 추가한다.
  2. UserDetailsService라는 이름의 빈을 생성한다. 이 때, UserDetailsService 빈은 사용자 인증에 수행할 user의 username, randomly generated password, role 등의 정보를 담고 있어야 한다.
  3. AuthenticationEventPublisher 빈을 생성한다. 인증 성공, 실패 등의 이벤트가 발생했을 때 이를 이벤트 리스너가 처리할 수 있도록 처리한다.

물론, Spring Security를 완전히 이해하지 못했다면 위 코드를 100퍼센트 이해하지 못한다. 아래 글을 통해서 Spring Security를 알아보자. 먼저 그 구조이다.

Authentication Process Architecture

아래 나오는 모든 사진은 Spring Security의 공식 docs에서 설명하고 있는 사진임을 미리 밝힌다. 또한, 현재의 설명은 Servlet 기반의 애플리케이션에서 설명하고 있는 architecture이다. Reactive 애플리케이션에 대해서는 다루고 있지 않다.

Filters(FilterChain)

Spring Security의 Servlet은 Servlet Filter에 기반을 두고 구현되었다. 하나의 HTTP request가 클라이언트로부터 servlet에 도달하기 위해 여러 개의 Filter를 거쳐야 하고 이를 Chain처럼 연결해 놓은 구조를 가진다(FilterChain).

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

만약, 클라이언트가 애플리케이션에 HTTP 요청을 보낸다면 Spring container에서 여러개의 Filter 인스턴스를 연결한 FilterChain을 생성하고 모든 Filter를 통과해야만 최종적으로 HttpServletRequest를 수행하는 Servlet에 도달하도록 자동으로 설정한다.

이때, FilterChain이 생성되는 기준은 클라이언트가 보낸 HTTP Request의 URI이다.

눈치 빠른 개발자들은 여기서 눈치챌 수 있는 점이 있다. 클라이언트가 보낸 HTTP Request URI를 기준으로 FilterChain이 생성되므로 특정 URI 패턴에 맞춰 별도의 Filter Chain을 구성할 수 있다.

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun publicSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .securityMatcher("/public/**") // "/public/**" 요청에 대해 적용
            .authorizeRequests { authorize ->
                authorize.anyRequest().permitAll() // 모든 요청 허용
            }
        return http.build()
    }

    @Bean
    fun adminSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .securityMatcher("/admin/**") // "/admin/**" 요청에 대해 적용
            .authorizeRequests { authorize ->
                authorize.anyRequest().hasRole("ADMIN") // ADMIN 역할 필요
            }
            .formLogin() // 폼 로그인 사용
        return http.build()
    }

    @Bean
    fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .authorizeRequests { authorize ->
                authorize.anyRequest().authenticated() // 기본적으로 인증 필요
            }
            .httpBasic() // 기본 HTTP Basic 인증 사용
        return http.build()
    }
}

독자의 수준을 무시하는 것은 아니지만 노파심에 언급하고 넘어가보자. Spring MVC 애플리케이션에서 Servlet은 DispatcherServlet의 인스턴스를 의미한다. 그러므로 최종적으로 하나의 Servlet에서는 무조건 단 하나의 HttpServletRequest와 HttpServletResponse만을 다룬다. 이때, FilterChain(하나 이상의 Security Filter)을 사용한다면 아래와 같은 동작이 가능하다.

  • 하위 Filter나 Servlet 호출을 막음: 특정 필터는 조건에 따라 필터 체인의 아래 단계(다른 필터나 Servlet)가 호출되지 않도록 방지할 수 있다. 이 때, Filter는 일반적으로 HttpServletResponse를 직접 작성한다.
  • 하위 Filter나 Servlet이 사용할 HttpServletRequest나 HttpServletResponse를 수정: HttpServletRequest에 사용자 인증 정보를 추가하거나, HttpServletResponse의 헤더를 설정할 수 있다.

아래 코틀린으로 작성된 코드를 참고하자. 이런식으로 사용자가 원하는 Filterchain을 적용할 수 있다.

fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
    // 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 구현의 순서에 주의하자.  Filter의 영향력은 무조건 FilterChain 아랫방향으로 내려간다. 정의된 순서에 따라 나오는 결과가 천차만별이니 인증 프로세스를 미리 설계하고 FilterChain을 구현하여 쓸데없는 디버깅을 방지하자.

DelegatingFilterProxy(Delegate; 위임하다)

Spring은 DelegatingfilterProxy라는 Filter 구현체를 제공한다. 이 특별한 filter는 Servlet 컨테이너의 라이프 사이클과 Spring ApplicationContext를 연결하는 역할을 한다.

🤔🤔🤔

ervlet 컨테이너가 관리하는 영역과 Spring ApplicationContext가 관리하는 영역은 서로 완전히 별개이다. Servlet 컨테이너는 톰캣, 제티와 같은 WAS로, 웹 요청과 응답의 라이프사이클을 관리한다. 이때, Servlet 컨테이너 입장에선 Spring Application 내에서 정의된 FilterService 같은 Bean들은 컨테이너의 직접적인 관리 대상이 아니므로 그 정보를 전혀 알 수 없다.

마찬가지로 Spring ApplicationContext는 Spring 애플리케이션에서 사용할 객체를 생성하고 관리하는 역할을 하여 기본적으로 Servlet 컨테이너와 독립적인 생명주기를 가지게 된다. 하지만 Filter의 일부 인증 구현이 Spring Application의 Bean을 적극적으로 활용해야 하는 상황이라면 이런 상황에서 구현에 제한이 생긴다. 이를 해결하기 위해 Spring에서는 DelegatingFilterProxy를 두고 두 독립된 환경을 연결하였다.

즉, DelegatingFilterProxy는 서블릿 컨테이너에서 등록되지만, Spring 컨텍스트의 필터 Bean에게 요청 처리를 위임하는 특별한 필터이다. 실제로는 아무런 작업도 처리하지 않고 단순히 요청을 Spring ApplicationContext에서 찾은 특정 Bean에게 전달하는 역할을 한다.

 

아래의 pseudo code로 이해해보자.

fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
	val delegate: Filter = getFilterBean(someBeanName)
	delegate.doFilter(request, response)
}

이 필터는 Request를 받을 때마다, “someBeanName”이라는 이름의 Filter 빈을 Spring ApplicationContext에서 가져온다. Spring에서 가져온 Filter 빈이 doFilter 메서드를 호출하여 요청과 응답을 처리하게 되는데, 실제 이 처리는 Servlet 컨테이너에서 이루어지는 것이 아니라 “someBeanName”으로 연결된 Spring ApplicationContext의 Bean에서 이루어진다.

이 독특한 구현이 아래와 같은 한 가지 특수한 효과를 불러올 수 있다.

Lazy Initialization(지연 초기화)로 인한 컨테이너 부팅 순서 이슈 해결

원칙적으로 서블릿 컨테이너는 모든 필터 인스턴스들을 애플리케이션 컨텍스트가 로딩되기 전에 먼저 등록해야 한다. 하지만 Spring Application Bean들은 ContextLoaderListener를 통해 애플리케이션 컨텍스트가 모두 로드된 이후에 생성된다.

이 시간차 문제를 Proxy로 해결한 것이다. DelegatingFilterProxy는 Request가 처음 들어올 때까지 Spring Context에서 필터 Bean을 찾지 않는 구조이다. 즉, Application이 시작될 때 미리 로드하지 않고, 실제로 요청을 받을 때 필요한 Bean을 가져온다.

이를 통해 Spring Application의 부팅 시점과 필터 등록 시점을 유연하게 처리할 수 있다.

FilterChainProxy

위에서 언급한 Proxy구조가 그 활용 방법이 굉장히 확장될 수 있기 때문에 Spring Security에서 가지는 의미가 크다.

스프링 시큐리티에서는 여러 개의 Filter Instance가 엮여 있는 다른 Security FilterChain을 불러올 수도 있다. 이때, FilterChainProxy는 Spring Bean이기 때문에 보통 DelegatingFilterProxy에 의해 감싸져서 사용된다. 이는 특정 보안 작업을 모듈화하고 다른 FilterChain에 유연하게 연결하여 사용하는 데 주된 용도가 있다.

SecurityFilterChain

위 설명과 이어지는 내용이다. SecurityFilterChain은 FilterChainProxy에 의해 연결되는 Spring SecuSecurity Filter 인스턴스이다.

Security Filter는 일반적으로 Spring Bean으로 등록되지만, DelegatingFilterProxy 대신 FilterChainProxy에 의해 등록된다. 비슷한 기능을 지원하는 두 개의 Proxy지만 FilterChainProxy가 선택되는 이유는 보안, 인증에 중요한 몇 가지 기능들을 추가로 제공해 줄 수 있기 때문이다.

1. Spring Security 서블릿 지원의 시작점:

FilterChainProxy는 Spring Security의 서블릿 지원의 시작점을 제공한다. Spring Security Servlet 지원을 디버깅하려면 FilterChainProxy에 디버그 포인트를 추가하는 것이 이상적인 방법이다.

2. Spring Security에서 필수적인 작업 수행:

FilterChainProxy는 Spring Security의 사용에서 가장 중심적인 역할을 하므로, 선택적으로 처리되지 않는 작업들을 수행할 수 있다. 예를 들어, 메모리 누수를 방지하기 위해 SecurityContext를 정리하거나, HttpFirewall을 적용하여 특정 종류의 공격으로부터 애플리케이션을 보호한다.

3. SecurityFilterChain의 실행 시점 유연성: FilterChainProxy는 SecurityFilterChain이 언제 실행될지를 결정하는 데 더 큰 유연성을 제공할 수 있다. 서블릿 컨테이너에서는 Filter 인스턴스들이 URL에 따라 실행되지만, FilterChainProxy는 RequestMatcher 인터페이스를 사용하여 HttpServletRequest의 특정 요소에 따라 실행 시점을 결정할 수 있다.

Spring 진영에서 가장 훌륭한 예시 아키텍처를 그려주었다. 위처럼, 요청 URI의 패턴에 의해 SecurityFilterChain을 다르게 적용할 수 있다. 물론 이뿐만 아니라 다양한 요소(예: HTTP 메서드, 헤더, 파라미터, User Role, 접속 IP 등)를 기반으로 필터 체인을 분리할 수도 있다. 이를 가능하게 하기 위해서 RequestMatcher라는 인터페이스가 존재한다. RequestMatch 인터페이스는 다양한 예시를 가지고 있다.

  • AntPathRequestMatcher: URI 패턴에 따라 매칭. 예를 들어, "/admin/**" 패턴으로 특정 URL 경로에만 필터 체인을 적용
  • HttpMethodRequestMatcher: HTTP 메서드(GET, POST 등)에 따라 필터 체인을 다르게 설정
  • IpAddressMatcher: IP 주소를 기반으로 필터 체인을 매칭. 특정 IP 주소에서의 요청에만 다른 필터 체인을 적용
  • ELRequestMatcher: SpEL 표현식을 사용하여 복잡한 조건을 정의. 이로 인해 요청의 여러 요소(예: 헤더, 파라미터, 사용자 정보 등)를 조합하여 필터 체인을 분리 가능

User Role이 “ADMIN”이거나, 특정 IP 주소에서 접근할 때만 별도의 필터 체인을 적용하는 예시.

@Configuration
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {
        http
            .securityMatcher(RequestMatcher { request ->
                val userRole = request.userPrincipal?.authorities?.any { it.authority == "ROLE_ADMIN" } ?: false
                val isTrustedIp = IpAddressMatcher("192.168.1.0/24").matches(request)
                userRole || isTrustedIp
            })
            .authorizeRequests {
                it.anyRequest().authenticated()
            }
            .formLogin()
    }
}

다시 SecurityFilterChain으로 돌아와서, 아키텍처 그림에 집중해보자. SecurityFilterChain 0은 세 개의 Security Filter 인스턴스를 가지고 있고 SecurityFilterChain n은 네 개의 Seucity Filter 인스턴스를 가지고 있다.

각각의 SecurityFilterChain은 서로 간에 아무런 의존성도 가지고 있지 않고 독립되어 설정할 수 있다. 개발자의 필요에 의해서라면, Application에서 Spring Security가 특정 요청을 무시하길 원한다면 아무런 보안 필터를 거치지 않게도 설정할 수 있다(이렇게 하는 경우가 얼마나 있겠냐 싶지만…).

Security filter

Security Filter는 SecurityFilterChain을 구성하는 인스턴스이다. SecurityFilterChain의 API를 사용하여 FilterChainProxy에 삽입된다. 이런 Filter들은 인증, 인가, 외부 공격 방어 등 상당히 다양한 목적으로 사용될 수 있다. FilterChain과 다르게 개발자가 선언한 순서가 아니라 미리 정의된 순서로 실행되어 올바른 시점에 호출되도록 보장한다.

예를 들어, 인증을 수행하는 필터는 반드시 권한 부여를 수행하는 필터보다 먼저 호출되어야 한다. 이러한 작업은 개발자가 일일이 지정할 필요 없이 Spring Security에서 자동으로 담당해준다. 따라서 일반적인 경우에는 Spring Security의 필터 순서를 알아야 할 필요는 없다.

만약 순서를 알고 싶다면 아래 FilterOrderRegistration class를 확인해보자.

 

spring-security/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java at

Spring Security. Contribute to spring-projects/spring-security development by creating an account on GitHub.

github.com

그러므로 개발자는 필요한 Filter 요소들을 단지 “선언”만 해주면 된다.

@Configuration
@EnableWebSecurity
class SecurityConfig {

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

}

위 예시 코드의 경우에는 아래와 같은 설정이 Filter의 순서에 영향을 끼치게 된다.

Filter Added by
CsrfFilter HttpSecurity#csrf
UsernamePasswordAuthenticationFilter HttpSecurity#formLogin
BasicAuthenticationFilter HttpSecurity#httpBasic
AuthorizationFilter HttpSecurity#authorizeHttpRequests

먼저 CsrfFilter를 통해 CSRF 공격을 방어하고 둘째로 인증 필터를 호출하여 요청을 인증한다. 마지막으로 Authorizationfilter를 호출하여 요청을 승인한다.

Security filter 출력하기

종종, Security Filter를 직접 출력하여 확인하면서 특정 요청에 대한 필터 조건을 확인할 때가 있다. 주로 개발단계에서 특정 요청 URI에 대해서 의도한 필터를 모두 지나가나 확인하거나 어디서 예외가 발생한 지 확인할 때 필요한 작업일 것이다.

filter 조건은 Application에서 INFO 레벨의 로그로 출력되므로 콘솔 출력으로 이를 확인할 수 있다.

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]

Filter Chain에 Custom Filter 추가하기

대부분의 경우에 기본으로 설정할 수 있는 SecurityFilter는 애플리케이션의 보안 인증을 구현하는 데에 문제가 없을 것이지만 꼭 필요한 필터 단계가 있다면 개발자의 의도대로 커스텀 된 Filter를 끼워 넣을 수도 있다.

아래 코드를 보자.

class TenantFilter : Filter {

    @Throws(IOException::class, ServletException::class)
    override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, filterChain: FilterChain) {
        val request = servletRequest as HttpServletRequest
        val response = servletResponse as HttpServletResponse

        val tenantId = request.getHeader("X-Tenant-Id") // (1)
        val hasAccess = isUserAllowed(tenantId) // (2)
        if (hasAccess) {
            filterChain.doFilter(request, response) // (3)
        } else {
            throw AccessDeniedException("Access denied") // (4)
        }
    }
}

예시 코드에서는 현재 요청의 tenant Id 헤더를 읽고 접속한 사용자가 tenant에 접근 권한이 있는지 확인하는 Filter이다. 현재 예시 코드의 상황은 현재 사용자가 누군지 알고 있기 때문에(인증 작업을 거치고 왔기 때문에), authentication filter 뒤에 해당 필터를 삽입해야 한다.

@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http
        // ...
        .addFilterBefore(TenantFilter(), AuthorizationFilter::class.java)
    return http.build()
}

따라서 이 필터를 AuthorizatoinFilter 뒤에 삽입하는 API를 호출하면 개발자가 의도한 Custom Filter rule이 완성된다.

물론 필요에 따라서 HttpSecurity#addFilterAfter이나 HttpSecurity#addFilterAt 등의 API를 활용하여 다양하게 FilterChain을 구성할 수 있다.

 

Filter를 Spring Bean으로 선언하는 과정에서 한 가지 주의해야 할 점이 있다. @Component로 필터를 애노테이션하거나, Configuration 파일에서 빈으로 선언하는 경우 Spring Boot가 이 Filter를 내장된 컨테이너에 자동으로 등록하게 된다. 이 경우에 필터가 Servlet 컨테이너에 의해 한 번, Spring Security에 의해 또 한번, 총 두 번 호출될 수 있으며, 호출 순서도 달라질 수 있다.

따라서, 의존성 주입을 위해 Filter를 Spring Bean으로 선언해야 할 경우, 중복 호출을 방지하기 위해서 FilterRegistrationBean을 사용하여 컨테이너에 등록되지 않도록 설정할 수 있다. FilterRegistrationBean의 enabled 속성을 false로 하면 Spring boot가 필터를 Servlet 컨테이너에 등록하지 않게 된다. 아래 코드를 참고하자.

@Configuration
class FilterConfig {

    @Bean
    fun tenantFilterRegistration(filter: TenantFilter): FilterRegistrationBean<TenantFilter> {
        val registration = FilterRegistrationBean(filter)
        registration.isEnabled = false
        return registration
    }
}

Handling Security Exceptions

ExceptionTranslationFilter는 Security Filter 중 하나로 FilterChainProxy에 삽입되어 AccessDeniedExcpetion과 AuthenticationException을 HTTP response로 변환할 수 있도록 해준다.

 

아래 아키텍처를 살펴보자.

순서대로 따라가 보자.

  1. ExceptionTranslationFilter는 FilterChain.doFilter(request, response)를 호출하여 애플리케이션의 나머지를 호출한다.
  2. 만약 사용자가 인증이 되지 않았거나 AuthenticationException이 발생했다면 Authentication 작업을 시작한다.(’Start Authentication’)
  3. 반면에 사용자가 AccessDeniedException을 받았다면 접근을 차단하고 해당 상황에 대한 핸들러를 실행한다.

이때, 애플리케이션이 AccessDeniedExcpetion 또는 AuthenticationException을 발생시키지 않는 한, ExcpetionTranslationFilter는 아무런 동작도 작업하지 않는다. 아래 코드는 ExceptionTranslationFilter의 의사 코드를 표현한 것이다. 실제 내부 구현 코드와 거리감이 있지만 이런 느낌으로 동작한다.

try {
    filterChain.doFilter(request, response)
} catch (ex: Exception) {
    when (ex) {
        is AccessDeniedException, is AuthenticationException -> {
            if (!authenticated || ex is AuthenticationException) {
                startAuthentication()
            } else {
                accessDenied()
            }
        }
        else -> throw ex // 다른 예외는 그대로 던짐
    }
}

Saving Request Between Authentication

위 아키텍처에서 인증 부분을 다시 살펴보자.

Authentication을 만족하지 못한 요청에 대해서 ExceptionTranslationFilter가 Authentication 작업으로 Request를 유도했다. 인증이 필요한 자원에 대해서 Request가 인증 정보 없이 들어왔을 때, 인증이 성공한 후 원래 요청을 다시 시도할 수 있도록 해당 요청을 cache 할 필요가 있다. Spring Security에서는 RequestCache 구현체를 사용하여 HttpServletRequest를 저장한다.

RequestCache

HttpServletRequest는 RequestCache에 저장된다. 사용자가 인증에 성공하면 RequestCache는 원래의 요청을 재생하여 사용한다. 기본적으로 httpRequestCache의 구현체로 HttpSessionRequestCache가 사용된다. 아래 예시 코드를 보자.

@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
    val httpRequestCache = HttpSessionRequestCache()
    httpRequestCache.setMatchingRequestParameterName("continue")
    http {
        requestCache {
            requestCache = httpRequestCache
        }
    }
    return http.build()
}

httpRequestCache에 저장된 요청 매개변수 이름이 “continue”일 경우 저장된 요청을 확인하기 위해 사용하는 RequestCache구현을 커스터마이징 하고 있다.

요청을 저장하지 않고 싶다면? NullRequestCache

예를 들어, 요청을 사용자의 브라우저로 옮기거나 DB에 저장하고 싶은 경우, 또는 로그인 전 사용자가 방문하려 했던 페이지로 리다이렉션 하지 않고 항상 홈 페이지로 이동하게 하려는 경우 사용자의 인증되지 않은 요청을 세션에 저장하지 않고 싶을 수도 있다.

이경우, NullRequestCache 구현체를 사용할 수 있다. 아래 예시 코드를 확인하자.

@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
    val nullRequestCache = NullRequestCache()
    http {
        requestCache {
            requestCache = nullRequestCache
        }
    }
    return http.build()
}

RequestCacheAwareFilter

RequestCacheAwareFilter는 RequestCache를 사용하여 원래 요청을 재시도한다.

Logging

Spring Security는 모든 보안 관련 이벤트에 대해서 DEBUG, TRACE 레벨까지 종합적인 로그 정보를 제공한다. Spring Security의 특성상 Response Body에 왜 요청이 거절되었는지에 대한 세부 정보를 담지 않기 때문에 이런 로그 체계는 디버깅하는 데에 있어서 중요한 정보를 가진다. 만약 401 또는 403 에러를 만났다면 무슨 상황인지 빠르게 파악하기 위해선 Spring Security의 로그 정보를 찾는 것이 가장 빠르다.

한 가지 예시를 들어보자. 유저가 CSRF에 대한 방어 체계를 가지고 있는 자원에 CSRF token이 없는 채로 POST 요청을 보냈다. 로그가 없다면 유저는 Spring Security에서 반환한 403 error를 만나게 될 것이고 요청이 왜 거절되었는지에 대한 아무런 정보를 받을 수 없을 것이다. 그러나, Spring Security에 남은 로그를 본다면 아래 정보를 확인할 수 있다.

2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Securing POST /hello
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking DisableEncodeUrlFilter (1/15)
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderFilter (3/15)
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking HeaderWriterFilter (4/15)
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : Invoking CsrfFilter (5/15)
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter         : Invalid CSRF token found for <http://localhost:8080/hello>
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl   : Responding with 403 status code
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]

댓글