(번역) Spring Security Architecture

급식·2025년 1월 20일
0

Spring

목록 보기
2/3

이번 섹션에서는 서블릿 기반 애플리케이션 내부를 기준으로 스프링 시큐리티의 아키텍처를 고수준으로 다루어볼 것입니다. 이번 섹션을 통해, 인증, 인가, 취약점 방어에 대한 전반적인 이해를 갖출 수 있을 것입니다.


A Review of Filters

스프링 시큐리티의 서블릿 지원은 서블릿 필터에 기반하므로, 필터의 기본적인 역할부터 이해하는 것이 도움이 될 것입니다. 아래 이미지는 하나의 HTTP 요청을 처리하기 위한 전형적인 핸들러 계층을 나타냅니다.

클라이언트가 애플리케이션에 요청을 보내면, 컨테이너는 Filter 인스턴스와 요청된 URI의 경로에 따라 HttpServletRequest를 처리하는 Servlet, 이 두 가지를 포함하는 FilterChain을 생성합니다.

이때 스프링 MVC 애플리케이션에서 ServletDispatcherServlet의 인스턴스이며, 하나의 서블릿은 기본적으로 하나의 HttpServletRequestHttpServletResponse만을 처리할 수 있지만 아래와 같이 두 개 이상의 Filter가 사용될 수 있습니다.

  • Servlet의 다운 스트림 Filter 인스턴스가 실행되는 것을 막을 수 있습니다. 이러한 경우 FilterHttpServletResponse를 조작합니다.
  • 다운 스트림 Filter 인스턴스나 Servlet에서 사용하는 HttpServletRequestHttpServletResponse를 조작하는 데 사용합니다.

Filter의 진가는 Filter에 전달되는 FilterChain에서 나옵니다.

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
}

Filter는 다운 스트림 Filter 인스턴스와 Servlet에만 영향을 주기 때문에, Filter의 실행 순서는 매우 중요합니다.


DelegatingFilterProxy

스프링은 서블릿 컨테이너의 생명 주기와 스프링의 ApplicationContext 사이를 연결해주는 DelegatingFilterProxy라는 이름의 Filter 구현체를 제공합니다. 서블릿 컨테이너는 자체 표준을 사용하여 Filter를 등록할 수 있게 해주지만, Spring에서 정의된 빈에서는 이를 인식할 수 없기 때문입니다. 따라서 표준 서블릿 메커니즘을 통해 DelegatingFilterProxy를 등록하면, 이는 곧 Filter를 구현하는 Spring Bean에 작업을 필터의 작업을 위임하는 것입니다.

아래 이미지는 Filter 인스턴스와 FilterChainDelegatingFilterProxy와 연계되는지 설명합니다.

DelegatingFilterProxyBean Filter 0ApplicationContext로부터 찾아(look-up) Bean Filter 0을 실행하는 역할을 수행합니다. 아래 의사 코드는 DelegatingFilterProxy의 대략적인 작동 방식을 설명합니다.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
	Filter delegate = getFilterBean(someBeanName);
	delegate.doFilter(request, response);
}
  1. 스프링 빈으로 등록된 Filter를 lazy하게 탐색합니다. 예제에서는 Filter 0의 인스턴스의 실행이 위임되었습니다.
  2. 스프링 빈의 호출을 대신 수행합니다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) 
        throws ServletException, IOException {
    
    // Lazily initialize the delegate if necessary.
    Filter delegateToUse = this.delegate;
    if (delegateToUse == null) {
    	this.delegateLock.lock();
        try {
        	delegateToUse = this.delegate;
            if (delegateToUse == null) {
            	WebApplicationContext wac = findWebApplicationContext();
                if (wac == null) {
                	throw new IllegalStateException("No WebApplicationContext found: " +
    								"no ContextLoaderListener or DispatcherServlet registered?");
    				}
                    delegateToUse = initDelegate(wac);
    			}
                this.delegate = delegateToUse;
    		}
            finally {
            	this.delegateLock.unlock();
            }
    	}
    
    	// Let the delegate perform the actual doFilter operation.
    	invokeDelegate(delegateToUse, request, response, filterChain);
    }

Double checking을 사용하여 race condition을 방지했고, 필요한 시점에 Filter를 구현한 스프링 빈 객체를 application context로부터 찾아 실행할 대상으로 등록(멤버 변수 초기화) 해주고, 락을 해제한다.
위 작업은 한 번만 수행되며 이후로는 실행할 필터 인스턴스를 호출해준다.

DelegatingFilterProxy를 사용했을 때의 또 다른 장점으로는, Filter 빈 인스턴스를 탐색하는 시점을 늦출 수 있다는 점입니다(lazy). 이는 컨테이너가 실행될 수 있는 상태가 되기 전에 Filter 인스턴스를 미리 컨테이너에 등록해두어야 하기 때문에 중요한데, 자세히 설명하자면 ContextLoadderListener가 스프링 빈을 로드하기 위해선 Filter 인스턴스가 반드시 먼저 등록되어야 하기 때문입니다.


FilterChainProxy

스프링 시큐리티의 서블릿 지원은 FilterChainProxy를 포함하고 있습니다. FilterChainProxySecurityFilterChain을 통해 다수의 Filter 인스턴스의 작동을 위임하기 위해 제공되는 스프링 시큐리티만의 특수한 Filter입니다. 이때 FilterChainProxy 역시 빈이므로, DelegatingFilterProxy에 감싸여져 있습니다.

아래 이미지는 FilterChainProxy의 역할을 설명합니다.


SecurityFilterChain

SecurityFilterChainFilterChainProxy에 의해 현재 요청을 처리하기 위해 어떤 스프링 시큐리티 Filter 인스턴스를 호출해야 할지 결정하기 위해 사용됩니다.

아래 이미지는 SecurityFilterChain의 역할을 설명합니다.

SecurityFilterChain 내부의 시큐리티 필터는 보통 빈이지만, 일반적으로 DelegatingFilterProxy보단 FilterChainProxy에 의해 등록됩니다. FilterChainProxy는 서블릿 컨테이너, 또는 DelegatingFilterProxy에 직접 등록하는 방식보다 여러 가지 장점을 가지고 있습니다. 첫째로, 모든 스프링 시큐리티의 서블릿 지원에 앞서는 진입점이 됩니다. 이 덕분에 스프링 시큐리티의 서블릿 지원 영역에서 트러블 슈팅을 해야 할 경우, 디버깅 포인트를 FilterChainProxy에 넣어 더욱 편하게 작업할 수 있습니다.

둘째로, FilterChainProxy가 스프링 시큐리티의 핵심이므로 반드시 실행되는 로직을 마음 편히 포함시킬 수 있습니다. 예를 들어 메모리 누수를 방지하기 위해 SecurityContext를 초기화하거나, 특정 유형의 공격으로부터 애플리케이션을 보호하기 위해 스프링 시큐리티의 HttpFirewall이 적용되는 등의 작업이 이루어집니다.

마지막으로, SpringFilterChain의 실행을 더욱 유연하게 할 수 있습니다. 서블릿 컨테이너에서 Filter 인스턴스는 URL 하나만 보고 실행되지만, FilterChainProxyRequestMatcher 인터페이스를 사용함으로써 HttpServeltRequest에 담긴 어떠한 정보라도 실행 여부를 결정하는 데 사용할 수 있습니다.

아래 이미지는 다수의 SecurityFilterChain 인스턴스의 작동을 설명하고 있습니다.

위의 그림에서 FilterChainProxy는 어떤 SecurityFilterChain이 사용되어야 할지 결정하고 있습니다. 가장 먼저 매칭되는 SecurityFilterChain만 실행되기 때문에, 만약 URL이 /api/messages/ 인 요청이 올 경우 SpringFilterChain 0의 패턴인 /api/**에 매칭되기 때문에 이후에 매칭되는 SpringFilterChain n이 아닌 SpringFilterChain 0이 실행됩니다. 반면 URL이 /messages/인 요청이 올 경우, SecurityFilter 0의 패턴에 매칭되지 않고 SecurityFilterChain n에 매칭됩니다.

여기서 중요한 점은 SecurityFilterChain 0에는 3개의 시큐리티 Filter 인스턴스만 등록되어 있는 반면 SecurityFilterChain n에는 4개의 Filter 인스턴스가 등록되어 있다는 것입니다. 즉, 각각의 SecurityFilterChain은 고유하며, 서로 독립적으로 설정될 수 있습니다. 사실 스프링 시큐리티가 특정 요청에 대해 아예 작동하지 않길 원한다면 SecurityFilterChain에 어떠한 Filter도 등록되지 않도록 하면 됩니다.


Security Filters

시큐리티 필터는 SecurityFilterChain API를 통해 FilterChainProxy로 삽입됩니다. 삽입된 필터들은 각각 취약점 방어, 인증, 인가와 같이 각기 다른 수많은 작업들을 수행할 수 있습니다. 이 필터들은 각각이 적절한 시점에 실행되도록 특정한 순서를 보장하는데, 예를 들어 인증을 수행하는 필터는 인가를 수행하는 필터 이전에 실행되어야 하는 식입니다. 보통은 스프링 시큐리티의 필터가 작동되는 순서에 대해 알고 있을 필요는 없지만, 궁금하시다면 FilterOrderRegistration 코드를 확인해보세요.

이러한 시큐리티 필터들은 대부분 HttpSecurity 인스턴스를 사용하여 등록되는데, 아래와 같은 보안 구성을 고려해 보겠습니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults())
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults())
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            );

        return http.build();
    }

}

위의 설정은 아래와 같은 필터 순서를 가집니다.

Filter추가 메서드
CsrfFilterHttpSecurity#csrf
UsernamePasswordAuthenticationFilterHttpSecurity#formLogin
BasicAuthenticationFilterHttpSecurity#httpBasic
AuthotizationFilterHttpSecurity#authorizeHttpRequests
  1. CsrfFilter가 CSRF 공격을 방어하기 위해 실행됩니다.
  2. 요청에 대한 인증이 수행됩니다.
  3. 요청에 대한 인가가 수행됩니다.

ℹ️ 참고
위에 표시되지 않은 필터 인스턴스가 더 있습니다. 적용되고 있는 다른 필터를 직접 출력해 보세요.

시큐리티 필터 출력하기

특정 요청에 대해 호출되고 있는 시큐리티 필터의 목록을 확인하는 것이 도움이 될 때가 종종 있습니다. 예를 들어 임의로 추가해 놓은 필터가 제대로 등록되었는지 확인해야 하는 상황 등에 유용합니다.

애플리케이션이 시작될 때 DEBUG 레벨에서 필터의 목록이 출력되므로, 아래와 비슷한 콘솔 출력을 확인할 수 있을 것입니다.

2023-06-14T08:55:22.321-03:00  DEBUG 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [ DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter]

이를 확인함으로써 필터 체인이 어떻게 구성되어 있는지 한 눈에 파악할 수 있습니다.

나아가 각 요청에 대해 각 개별 필터의 호출을 출력하도록 애플리케이션을 구성할 수도 있습니다. 이는 특정한 요청에 대해 필터를 추가했거나, 예외가 발생하는 지점을 확인할 때 유용할 것입니다. 이는 애플리케이션에서 시큐리티 이벤트를 로깅하도록 설정하면 가능합니다.

필터 체인에 필터 추가하기

대부분의 경우 기본적으로 제공되는 시큐리티 필터는 애플리케이션의 모든 보안 요구 사항을 해결해주지 못합니다. 따라서 SecurityFilterChain에 커스텀 필터를 추가해야 할 것입니다.

HttpSecurity는 필터를 다음 세 가지 메서드로 등록할 수 있도록 해줍니다.

  • #addFilterBefore(Filter, Class<?>) : 커스텀 필터를 특정 필터 이전에 추가
  • #addFilterAfter(Filter, Class<?>) : 커스텀 필터를 특정 필터 이후에 추가
  • #addFilterAt(Filter, Class<?>) : 커스텀 필터를 특정 필터와 교체

커스텀 필터 추가하기

필터를 직접 만들었다면, 필터 체인에서의 위치도 지정해 주어야 합니다. 이 작업을 위해 필터 체인에서 발생하는 다음의 핵심 이벤트를 눈여겨 보세요.

  1. SecurityContext가 세션에서 로딩됩니다.
  2. 요청이 secure header, CORS, CSRF 등의 흔한 취약점으로부터 보호됩니다.
  3. 요청이 인증됩니다. (authentication)
  4. 요청이 인가됩니다. (authorization)

필터를 삽입할 위치를 결정하려면, 다음과 같이 어떤 이벤트가 어느 시점에 발생하는지 고려해 보아야 합니다.

당신의 필터가..다음 필터 다음에 배치하세요.그렇다면 다음 이벤트들은 이미 발생한 것입니다.
취약점을 방어하는 필터라면SecurityContextHolderFilter1
인증 필터라면LogoutFilter1, 2
인가 필터라면AnonymousAuthenticationFilter1, 2, 3

💡 팁
대부분의 경우 애플리케이션은 커스텀 인증 필터를 필요로 합니다. 즉, 이들을 LogoutFilter 뒤에 배치하세요.

예를 들어, 해당 테넌트에 현재 사용자가 접근할 수 있는지 헤더의 테넌트 ID를 읽는 필터를 추가해야 한다고 가정해 봅시다.

먼저, Filter를 하나 만들어 보겠습니다.

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;

public class TenantFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

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

}

위의 샘플 코드는 다음과 같이 작동합니다.

  1. 요청의 헤더로부터 테넌트 id를 가져 옵니다.
  2. 현재 사용자가 테넌트 id에 접근 가능한지 확인합니다.
  3. 만약 그렇다면, 체인의 다음 필터를 실행합니다.
  4. 만약 그렇지 않다면, AccessDeniedException을 던집니다.

💡 팁
Filter를 직접 구현하는 대신, OncePerRequestFilter를 구현하여 HttpServletRequestHttpServletResponse 파라미터를 가지는 doFilterInternal 메서드를 통해 요청 당 단 한 번만 실행되도록 보장하세요.

이제 만들어진 필터를 SecurityFilterChain에 추가할 차례입니다. 이미 언급했듯, 사용자가 누구인지 일단 식별해야 하므로 인증 필터 뒤에 배치하겠습니다.

위에서 제시된 가이드 라인에 의하면, 인증 필터 중 가장 마지막인 AnonymousAuthenticationFilter의 뒤에 다음과 같이 배치하는 것이 좋겠습니다.

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); // (1)
    return http.build();
}
  1. HttpSecurity#addFilterAfter를 사용하여 TenantFilterAnonymousAuthenticationFilter의 뒤에 추가하였습니다.

필터를 AnonymousAuthenticationFilter의 뒤에 추가함으로써, TenantFilter가 모든 인증 필터가 실행된 이후에 실행됨이 보장되었습니다.

따라서, 이제 TenantFilter가 필터 체인에서 호출되어 현재 사용자가 테넌트 ID에 접근할 수 있는지 확인하게 되었습니다.

필터를 빈으로 등록하기

Filter를 스프링 빈으로 등록할 때, @Component 어노테이션을 붙이거나 기타 설정을 사용하면 스프링 부트가 자동으로 내장 컨테이너에 이를 등록해 줍니다. 따라서 필터가 컨테이너에 의해 한 번, 그리고 스프링 시큐리티에 의해 한 번, 총 두 번 다른 순서대로 실행될 수도 있습니다. 이 때문에 필터는 스프링 빈으로 등록하지 않기도 합니다.

하지만 필터를 의존성 주입 등의 이유로 스프링 빈으로 등록하고자 한다면, 스프링 부트가 이를 등록하지 못하도록 FilterRegistrationBean 빈과 함께 enabled 옵션을 false로 하여 선언하면 됩니다.

@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}

위와 같이 작성하면, HttSecurity만이 이를 빈으로 등록합니다.

스프링 시큐리티 필터 커스터마이징하기

일반적으로, 스프링 시큐리티의 필터를 설정하기 위해 필터의 DSL 메서드를 사용할 수 있습니다. 예를 들어 BasicAuthenticationFilter는 DSL로 다음과 같이 표현됩니다.

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		.httpBasic(Customizer.withDefaults())
        // ...

	return http.build();
}

하지만 스프링 시큐리티 필터를 직접 설정하려는 경우에는 다음과 같이 addFilterAt을 사용하여 DSL로 지정할 수 있습니다.

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
	// ... configure

	http
		// ...
		.addFilterAt(basic, BasicAuthenticationFilter.class);

	return http.build();
}

여기서 중요한 점은, 필터가 이미 추가되었다면 스프링 시큐리티가 예외를 던진다는 것입니다. 예를 들어 HttpSecurity#httpBasic은 당신이 작성한 BasicAuthenticationFilter를 등록해주지만 BasicAuthenticationFilter 자리에 두 번 배치하려 시도하려는 시도는 실패할 것입니다.

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
	// ... configure

	http
		.httpBasic(Customizer.withDefaults())
		// ... on no! BasicAuthenticationFilter is added twice!
		.addFilterAt(basic, BasicAuthenticationFilter.class);

	return http.build();
}

이러한 경우, 커스텀한 BasicAuthenticationFilter를 만들었으므로 httpBasic의 호출을 제거해야 합니다.

💡 팁
특정 필터를 추가하지 않도록 HttpSecurity의 설정을 덮어 쓸 수 없는 경우, 일반적으로 다음과 같이 해당 DSL의 disable 메서드를 호출하여 Spring Security 필터를 비활성화 할 수 있습니다.

.httpBasic((basic) -> basic.disable())


시큐리티 예외 처리하기

ExceptionTranslationFilterAccessDeniedExceptionAuthenticationException을 HTTP 응답으로 변환하는 역할을 수행합니다.

또한 ExceptionTranslationFilter는 시큐리티 필터의 일부로써 FilterChainProxy에 삽입됩니다.

아래 이미지는 ExceptionTranslationFilter와 다른 컴포넌트 사이의 관계를 설명합니다.

  1. ExceptionTranslationFilter가 다음 필터의 호출을 위해 FilterChain.doFilter(request, response)를 실행시킵니다.
  2. 만약 사용자가 인증되지 않았거나 AuthenticationException이 발생한다면, 인증을 시작합니다.
    1. SecurityContextHolder가 비워집니다.
    2. HttpServletRequest가 저장되어 인증이 성공했을 때 원래 하려고 했던 요청을 다시 처리할 수 있게 됩니다.
    3. AuthenticationEntryPoint가 클라이언트로부터 특정한 요청 인증값을 받아내기 위해 사용됩니다. 예를 들어, 로그인 페이지로 리다이렉트 시키거나 WWW-Authenticate 헤더를 보낼 수도 있습니다.
  3. 이외의 경우, AcessDeniedException이 발생했다면 접근이 거부된 것을 의미하므로 AccessDeniedHandler가 접근 거부를 처리하기 위해 호출됩니다.

ℹ️ 참고
애플리케이션이 AccessDeniedException이나 AuthenticationException을 던지지 않는다면 ExceptionTranslationFilter는 작동되지 않습니다.

ExceptionTransitionFilter의 작동을 의사 코드로 나타내면 대강 아래와 같습니다.

try {
	filterChain.doFilter(request, response); // (1)
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication(); // (2)
	} else {
		accessDenied(); // (3)
	}
}
  1. 맨 앞에서 설명했듯, FilterChain.doFilter(request, response)를 호출하는 것은 애플리케이션의 다음 실행 흐름을 호출하는 것과 동일합니다. 이는 즉, 애플리케이션의 다른 부분에서(FilterSecurityInterceptor나 다른 보안 메서드) AuthenciationException이나 AccessDeniedException을 던진다면 여기서 걸려 처리됨을 의미합니다.
  2. 사용자가 인증되지 않았거나 AuthenticationException이 발생했다면, 인증을 시작합니다.
  3. 실패한 경우, 접근이 거부됩니다.

인증 사이에서 요청 저장하기

직전의 ‘시큐리티 예외 처리하기’ 절에서, 요청이 인증되지 않았거나 요청된 자원이 인증을 필요로 하는 경우 인증에 성공한 다음 다시 요청을 처리하기 위해 요청 정보를 저장해야 할 필요가 있음을 배웠습니다. 스프링 시큐리티에서는 이를 RequestCacheHttpServletRequest를 저장함으로써 기능을 제공합니다.

RequestCache

HttpServletRequestRequestCache에 저장됩니다. 사용자가 인증에 성공하면, RequestCache가 원본 요청을 재시도 할 때 사용됩니다. RequestCacheAwareFilter는 사용자의 인증이 끝난 후에 저장되어 있던 HttpServletRequest를 가져오기 위해 RequestCache를 사용합니다. 반면 ExceptionTranslationFilterAuthenticationException을 감지한 후, 사용자를 로그인 엔드 포인트로 리다이렉팅 시키기 전에 HttpServletRequest의 정보를 RequestCache에 저장합니다.

기본적으로는 HttpSessionRequestCache가 사용되며, 아래 코드는 continue라는 이름의 파라미터가 있는 경우 저장된 요청에 대해 HttpSession을 확인하는 데 RequestCache를 사용하는 예제입니다.

@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
	HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
	requestCache.setMatchingRequestParameterName("continue");
	http
		// ...
		.requestCache((cache) -> cache
			.requestCache(requestCache)
		);
	return http.build();
}

요청이 저장되는 것을 막는 방법

사용자의 인증된 요청을 세션에 남기고 싶지 않을 수도 있을 것입니다. 예를 들자면, 인증 정보의 저장소를 사용자의 브라우저나 데이터베이스로 하고 싶거나, 사용자가 로그인 전에 방문하려고 했던 페이지 대신 항상 홈 페이지로 리디렉션 하고 싶은 경우 해당 기능을 꺼야 할 것입니다.

이는 NullRequestCache의 구현체를 사용함으로써 가능합니다.

@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    RequestCache nullRequestCache = new NullRequestCache();
    http
        // ...
        .requestCache((cache) -> cache
            .requestCache(nullRequestCache)
        );
    return http.build();
}

RequestCacheAwareFilter

RequestCacheAwareFilterRequestCache를 원본 요청을 재현하는 데 사용됩니다.


Logging

스프링 시큐리티는 DEBUG, 또는 TRACE 레벨에서 모든 시큐리티 관련 이벤트에 대한 포괄적인 로깅을 제공합니다. 스프링 시큐리티는 보안 조치를 위해 요청이 거부된 이유에 대한 세부 정보를 응답의 body에 추가하지 않기 때문에, 애플리케이션을 디버깅할 때 유용하게 사용될 수 있습니다. 만약 401이나 403 에러를 받게 된다면, 로그를 참고하여 어떤 문제가 있었는지 추적할 수 있을 것입니다.

사용자가 CSRF 토큰 없이 CSRF 프로텍션이 켜져 있는 상태에서 POST 메서드로 자원을 요청했다고 가정해 봅시다. 로그가 없다면, 사용자는 요청이 거부된 이유에 대한 어떠한 설명도 없이 403 에러를 받게 될 것입니다. 그러나 스프링 시큐리티에 대한 로그를 켜 놓는다면, 아래와 같은 로그 메시지를 받아 볼 수 있을 것입니다.

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]

이제야 CSRF 토큰이 없기 때문에 요청이 거부되었음을 확인할 수 있게 되었습니다.

애플리케이션에서 모든 시큐리티 이벤트를 로깅하도록 하려면, 아래와 같이 설정하시면 됩니다.

logging.level.org.springframework.security=TRACE
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- ... -->
    </appender>
    <!-- ... -->
    <logger name="org.springframework.security" level="trace" additivity="false">
        <appender-ref ref="Console" />
    </logger>
</configuration>
profile
그래 다 먹고 살자고 하는 건데,, 🥹

0개의 댓글

관련 채용 정보