Spring Security - 3. 시큐리티 동작 원리, SecurityFilterChain 내부 구조, SecurityContextHolder.

하쮸·2025년 1월 21일

1. 시큐리티 동작.


1-1. 사용자의 요청 → 컨트롤러. (시큐리티 의존성 없는 경우)

  • 클라이언트의 요청은 서버 컴퓨터의 WAS (톰캣)의 필터들을 통과한 뒤 스프링 컨테이너의 컨트롤러에 도달함.

1-2. 시큐리티 의존성의 추가.

  • 스프링 시큐리티가 사용자의 요청을 감시하고 통제하는 지점
    • WAS의 필터단에서 요청을 가로챈 후 시큐리티의 역할을 수행함.
      • WAS의 필터에 하나의 필터를 만들어서 넣고 해당 필터에서 요청을 가로챔.
      • 해당 요청은 스프링 컨테이너 내부에 구현되어 있는 스프링 시큐리티 감시 로직을 거침.
      • 로직을 마친 후 다시 WAS의 필터로 돌아와서 남아있는 필터를 수행함.

1-3. 스프링 시큐리티의 간단한 로직.

  • 스프링 시큐리티 로직은 여러개의 필터들이 나열된 필터 체인 형태로 구성되어 있음.
    • 시큐리티 필터 체인 : 일련의 과정들을 수행하는 필터들의 묶음.
  • 각각의 필터에서 CSRF, 인증, 인가, 로그아웃 등 여러 작업을 수행함.

  • 위와 같이 여러 개의 필터 체인을 가질 수도 있음.

1-4. 역할 및 특징.

  • DelegatingFilterProxy
    • 서블릿 컨테이너에서 관리되는 프록시용 필터.
    • Spring의 Filter 구현체로, 서블릿 컨테이너의 필터 생명 주기를 Spring의 ApplicationContext와 연결함.
    • "springSecurityFilterChain"이라는 스프링 빈을 찾아 요청을 위임함.
    • 특징.
      • 일반적인 서블릿 필터 중 하나로, 스프링 빈으로 등록된 필터(Filter)에게 요청을 위임함.
      • 서블릿 필터와 스프링 빈 사이의 다리(bridge) 역할을 수행함.
  • FilterChainProxy
    • Spring Security의 여러 개의 보안 필터를 관리하는 핵심 필터.
    • 특징
      • "springSecurityFilterChain"이라는 이름으로 빈 등록.
      • DelegatingFilterProxy로부터 요청을 위임받아 실제 보안 처리를 수행.
      • 여러 SecurityFilterChain을 관리하고, 요청에 따라 적합한 체인을 선택.
      • 이 필터 체인은 SecurityFilterChain에 의해 정의됨.
  • SecurityFilterChain
    • 보안 관련 필터들의 묶음을 나타내는 인터페이스.
    • 실제 시큐리티 로직이 처리되는 부분.
    • 특징.
      • 여러 보안 필터로 구성된 체인을 의미.
      • matches() 메서드로 요청이 해당 체인에 적합한지 확인.
      • getFilters() 메서드로 필터 목록을 제공.
      • 요청에 따라 다른 보안 로직을 처리할 수 있게 해줌.
      • 각 필터 체인은 요청 패턴에 맞는 필터를 구성하고, FilterChainProxy에 의해 선택됨.
  • 위 세 구성 요소는 함께 작동하여 Spring Security의 필터 기반 보안 아키텍처를 구현함.
    • DelegatingFilterProxy가 요청을 받아 FilterChainProxy에 전달하고, FilterChainProxy는 적절한 SecurityFilterChain을 선택하여 보안 처리를 수행함.

1-4-1. DelegatingFilterProxy.

package org.springframework.boot.autoconfigure.security.servlet;

import jakarta.servlet.DispatcherType;
import java.util.EnumSet;
import java.util.stream.Collectors;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

@AutoConfiguration(
    after = {SecurityAutoConfiguration.class}
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@EnableConfigurationProperties({SecurityProperties.class})
@ConditionalOnClass({AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class})
public class SecurityFilterAutoConfiguration {
    private static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";

    public 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;
    }

    private EnumSet<DispatcherType> getDispatcherTypes(SecurityProperties securityProperties) {
        return securityProperties.getFilter().getDispatcherTypes() == null ? null : (EnumSet)securityProperties.getFilter().getDispatcherTypes().stream().map((type) -> {
            return DispatcherType.valueOf(type.name());
        }).collect(Collectors.toCollection(() -> {
            return EnumSet.noneOf(DispatcherType.class);
        }));
    }
}
  • 위 클래스는 DelegatingFilterProxy를 Spring Security 필터 체인으로 등록하는 설정을 다룸.
    • DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean("springSecurityFilterChain", new ServletRegistrationBean[0]);
    • new 연산자와 생성자를 통해 DelegatingFilterProxyRegistrationBean을 생성.
    • 등록해놓은 BeanName, 즉 springSecurityFilterChain을 등록해놓으면 해당 빈으로 요청을 위임하게 됨.
package org.springframework.web.filter;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

public class DelegatingFilterProxy extends GenericFilterBean {
    @Nullable
    private String contextAttribute;
    @Nullable
    private WebApplicationContext webApplicationContext;
    @Nullable
    private String targetBeanName;
    private boolean targetFilterLifecycle;
    @Nullable
    private volatile Filter delegate;
    private final Lock delegateLock;
    
    
    ....
    
    protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        delegate.doFilter(request, response, filterChain);
    }
  • invokeDelegate 메서드에 브레이크 포인트를 걸고 디버그를 통해 확인 해보면.


1-4-2. FilterChainProxy.

package org.springframework.security.web;

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 java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

public class FilterChainProxy extends GenericFilterBean {
    private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
    private static final String FILTER_APPLIED = FilterChainProxy.class.getName().concat(".APPLIED");
    private SecurityContextHolderStrategy securityContextHolderStrategy;
    private List<SecurityFilterChain> filterChains;
    private FilterChainValidator filterChainValidator;
    private HttpFirewall firewall;
    private RequestRejectedHandler requestRejectedHandler;
    private ThrowableAnalyzer throwableAnalyzer;
    private FilterChainDecorator filterChainDecorator;

    public FilterChainProxy() {
        this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
        this.filterChainValidator = new NullFilterChainValidator();
        this.firewall = new StrictHttpFirewall();
        this.requestRejectedHandler = new HttpStatusRequestRejectedHandler();
        this.throwableAnalyzer = new ThrowableAnalyzer();
        this.filterChainDecorator = new VirtualFilterChainDecorator();
    }

    public FilterChainProxy(SecurityFilterChain chain) {
        this(Arrays.asList(chain));
    }

    public FilterChainProxy(List<SecurityFilterChain> filterChains) {
        this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
        this.filterChainValidator = new NullFilterChainValidator();
        this.firewall = new StrictHttpFirewall();
        this.requestRejectedHandler = new HttpStatusRequestRejectedHandler();
        this.throwableAnalyzer = new ThrowableAnalyzer();
        this.filterChainDecorator = new VirtualFilterChainDecorator();
        this.filterChains = filterChains;
    }
    
    .....
    
    
    private List<Filter> getFilters(HttpServletRequest request) {
        int count = 0;
        Iterator var3 = this.filterChains.iterator();

        SecurityFilterChain chain;
        do {
            if (!var3.hasNext()) {
                return null;
            }

            chain = (SecurityFilterChain)var3.next();
            if (logger.isTraceEnabled()) {
                ++count;
                logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, count, this.filterChains.size()));
            }
        } while(!chain.matches(request));

        return chain.getFilters();
    }
  • SecurityFilterChain 목록 : FilterChainProxy.
  • getFilters 메서드에 브레이크 포인트를 걸고 디버그를 통해 확인해보면.

  • 기본적으로 있는 DefaultSecurityFilterChain이 있고, 11개의 필터가 있는 모습을 확인할 수 있음.

1-4-3. SecurityFilterChain.

  • 스프링 시큐리티 의존성을 추가하면 기본적인 DefaultSecurityFilterChain 하나가 등록됨.
    • 커스텀마이징 하고 싶은 SecurityFilterChain을 등록하기 위해서는 SecurityFilterChain을 리턴하는 @Bean 메서드를 등록하면 됨.

SecurityFilterChain 한 개만 등록.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
        
        return httpSecurity.build();
    }
}

SecurityFilterChain 두 개 이상 등록.

@Configuration
@EnableWebSecurity
public class SecurityConfig {


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        
        httpSecurity.securityMatcher("/")
                .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/").permitAll());


        return httpSecurity.build();
    }

    @Bean
    public SecurityFilterChain filterChain2(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.securityMatcher("/admin")
                .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/admin").authenticated());
        return httpSecurity.build();
    }

filterChainProxy에서 SecurityFilterChain이 제대로 등록되어 있는 지 확인.


Authorize HttpServletRequests

  • securityMatcher

    • 클라이언트의 요청에 대한 최초 필터링을 하기 위한 기능.
    • httpSecurity.securityMatcher("/api/**") 하게 되면 클라이언트의 요청에 대해 /api/** 경로로만 시큐리티가 보안작동을 진행 시키겠다는 것.
      • 만약 /api/** 이외의 요청이 오게 되면 무시해 버림.
  • requestMatcher

    • httpSecurity.requestMatcher("/api/**")
    • 클라이언트의 모든 요청 경로에 대해 보안 작동을 진행하고 그 중에 /api/** 경로로 오는 것에 대해 보안 심사를 하겠다는 것.
    • /api/** 요청과 이외의 요청도 포함.
  • 그래서 securityMatcher 로 특정한 경로로만 보안기능을 작동하도록 한 다음 이 경로안에서 보안심사를 하고자 할 때 requestMatcher 를 사용한다고 생각하면 됨.

  • 복수개의 SecurityFilterChain을 설정할 시 순서를 선택할 수 있음.
    • @Order 어노테이션을 이용함.
@Configuration
@EnableWebSecurity
public class SecurityConfig {


    @Bean
    @Order(1)
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        
        httpSecurity.securityMatcher("/")
                .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/").permitAll());


        return httpSecurity.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain filterChain2(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.securityMatcher("/admin")
                .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/admin").authenticated());
        return httpSecurity.build();
    }
  • SecurityFilterChain을 거치게 된다면 내부적으로 여러 가지 필터를 거치게 되는데 이때 서버의 자원을 사용하고 상주 시간이 발생함.

    • 이때 특정 요청 경로를 보안 필터 체인에서 제외할 수 있음.
  • 보통 정적 리소스(이미지, CSS)의 경우 필터를 통과하지 않도록 아래 코드를 통해 설정할 수 있음.

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {

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

Interface WebSecurityCustomizer

  • ignoring() 메서드는 특정 요청 경로를 Spring Security의 필터 체인에서 제외시킴.
  • Spring Security 필터 체인을 통과하지 않고 요청이 바로 컨트롤러(혹은 리소스 서버)로 전달됨
    • 즉, 인증이나 권한 확인 없이 리소스를 반환.

2. SecurityFilterChain 내부 구조.

  • SecurityFilterChain은 스프링 시큐리티의 인증/인가 등과 같은 주요 보안 로직을 담당함.
    • 하나의 SecurityFilterChain 내부에는 N개의 필터가 존재하고 각각의 필터가 하나의 로직(로그아웃, 로그인, 인가 등)의 시발점이 됨.

2-1. SecurityFilterChain 내부 필터 확인하기.

@EnableWebSecurity(debug = true)
  • Security 설정 파일에서 해당 어노테이션에 debug = true로 설정하면 됨.
    • defaultValue는 false임.
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
}

  • 어떠한 설정도 없이 그저 의존성 추가와 어노테이션만 했을 경우 위와 같은 필터로 구성되어 있음.

  • DisableEncodeUrlFilter

    • URL 인코딩을 비활성화함.
    • URL에 세션 ID가 포함되지 않도록 방지함.
  • WebAsyncManagerIntegrationFilter

    • 비동기 요청 처리 시 SecurityContext와 Spring Web의 WebAsyncManager간 통합을 제공함.
      • 즉, Spring Web 비동기 요청 처리(@Async)를 SecurityContext와 통합함.
    • 비동기 요청에서도 인증 정보를 유지.
  • SecurityContextHolderFilter

    • SecurityContextRepository를 사용하여 SecurityContext를 가져와 SecurityContextHolder에 설정하는 필터.
  • HeaderWriterFilter

    • 현재 응답에 헤더를 추가하는 필터.
      • 브라우저 보호를 가능하게 하는 특정 헤더를 추가하는 데 유용.
        • X-Frame-Options, X-XSS-Protection 및 X-Content-Type-Options
  • CsrfFilter

    • CSRF(Cross-Site Request Forgery) 공격을 방지.
      • CSRF 토큰을 검증하여 악의적인 요청을 차단.
  • LogoutFilter

    • 주체(principal), 즉 사용자를 로그아웃 시킴.
    • 로그아웃이 완료된 후에는 LogoutSuccessHandler 또는 logoutSuccessUrl에 따라 결정된 URL로 리다이렉트가 수행됨.
  • UsernamePasswordAuthenticationFilter

    • 인증(authentication) 폼 요청을 처리함.
    • (참고) Spring Security 3.0 이전에는 이 필터가 AuthenticationProcessingFilter로 호출되었음.
    • 로그인 폼은 반드시 두 가지 매개변수(사용자 이름과 비밀번호)를 이 필터에 제공해야 함.
    • 해당 필터는 기본적으로 URL /login에 응답함.
  • DefaultResourcesFilter

    • CSS 또는 Javascript와 같은 기본 UI에 사용되는 정적 리소스를 처리하는 필터.
      • 내부용으로만 사용해야됨.
  • DefaultLoginPageGeneratingFilter

    • 사용자가 로그인 페이지를 구성하지 않은 경우, 네임스페이스 설정에서 내부적으로 사용됨.
    • 설정 코드가 이 필터를 필터 체인에 삽입함.
    • 해당 필터는 로그인 페이지로 리다이렉트가 되는 경우에만 동작함.
  • DefaultLogoutPageGeneratingFilter

    • 기본 로그아웃 페이지를 생성함.
  • BasicAuthenticationFilter

    • HTTP 요청의 BASIC 인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장함.
    • 요약하면, 해당 필터는 Basic 인증 방식과 Base64로 인코딩된 username:password 토큰을 포함한 Authorization HTTP 요청 헤더가 있는 모든 요청을 처리하는 역할을 함.
      • Ex) 사용자 "Aladdin"이 비밀번호 "open sesame"으로 인증하려는 경우 다음과 같은 헤더가 표시됨.
      Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
    • 인증이 성공하면, 결과로 생성된 Authentication 객체는 SecurityContextHolder에 저장됨.
    • 인증에 실패하고 ignoreFailure가 false(기본값)인 경우, AuthenticationEntryPoint 구현체가 호출됨.
      (단, ignoreFailure 속성이 true로 설정된 경우 제외).
      • 일반적으로, 이 구현체는 BasicAuthenticationEntryPoint여야 하며, 사용자는 다시 BASIC 인증을 통해 인증을 시도해야 함.
    • RememberMeServices가 설정된 경우, 이 필터는 자동으로 "Remember-Me" 세부 정보를 클라이언트에 전송함.
      • 따라서 이후 요청에서는 BASIC 인증 헤더를 다시 제공할 필요 없이 "Remember-Me" 메커니즘을 사용해서 인증됨.
  • RequestCacheAwareFilter

    • 저장된 요청이 캐시되어 있고 현재 요청과 일치하는 경우 저장된 요청을 재구성하는 일을 담당.
    • RequestCache에서 getMatchingRequest를 호출해서 메소드가 값(저장된 요청의 래퍼)을 반환하면 이를 필터 체인의 doFilter 메소드에 전달함.
      • 만약 캐시가 null을 반환하는 경우 원래 요청이 사용되며 필터는 적용되지 않음.
  • SecurityContextHolderAwareRequestFilter

    • 서블릿 API 보안 메서드를 구현하는 요청 래퍼로 서블릿Request를 채우는 필터.
  • AnonymousAuthenticationFilter

    • SecurityContextHolder에 Authentication 객체가 없는지 감지하고 필요한 경우 Authentication 객체로 채움.
  • ExceptionTranslationFilter

    • 필터 체인 내에서 발생한 모든 AccessDeniedException 및 AuthenticationException을 처리함.
      • AuthenticationException이 감지되면, 필터는 authenticationEntryPoint를 실행해서 이를 통해 AbstractSecurityInterceptor의 모든 하위 클래스에서 발생한 인증 실패를 공통적으로 처리할 수 있음.
      • AccessDeniedException이 감지되면, 필터는 사용자가 익명(anonymous) 사용자인지 여부를 판단해서 익명(anonymous) 사용자라면 authenticationEntryPoint가 실행되고 익명 사용자가 아니라면, 필터는 AccessDeniedHandler로 위임하고 기본적으로 이 필터는 AccessDeniedHandlerImpl을 사용함.
    • 해당 필터는 Java Exception와 HTTP 응답 간의 연결을 제공하므로 필요함.
  • AuthorizationFilter

    • AuthorizationManager를 사용하여 URL에 대한 접근(access)을 제한하는 authorization(인증) 필터.

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {

        return httpSecurity.build();
    }
}

  • 커스텀한 SecurityFilterChain을 등록했을 경우 위와 같은 필터로만 구성되어 있음.

2-2. 필터 활성화.

  • UsernamePasswordAuthenticationFilter 활성화.
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .formLogin(withDefaults());

        return httpSecurity.build();
    }
}


  • UsernamePasswordAuthenticationFilter, AuthorizationFilter 활성화.
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/user").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/login")
                        .permitAll()
                );

        return httpSecurity.build();
    }
}


  • BasicAuthenticationFilter 활성화.
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .httpBasic(Customizer.withDefaults());

        return httpSecurity.build();
    }
}


2-3. 커스텀 필터 등록.

httpSecurity.addFilterBefore(Filter(추가할 필터), Class<?>(기존 필터))
  • addFilterBefore
    • 기존 필터(Class<?>) 앞에 새로운 필터(Filter)를 추가.
httpSecurity.addFilterAfter(Filter(추가할 필터), Class<?>(기존 필터))
  • addFilterAfter
    • 기존 필터(Class<?>) 필터 뒤에 새로운 필터(Filter)를 추가.
httpSecurity.addFilterAt(Filter(추가할 필터), Class<?>(기존 필터))
  • addFilterAt
    • 기존 필터(Class<?>)를 새로운 필터(Filter)로 교체.

3. SecurityContextHolder.

package org.springframework.security.core;

import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
  • Authentication 객체는 실질적으로 인증 정보를 담고 있는 객체.
  • 서블릿 컨테이너에서 내부 인증 로직을 거친 후, 인증에 대한 정보들을 Authentication 객체에 담고 이를 SecurityContext 객체와 SecurityContextHolder 객체로 감싼 다음 최종적으로 SecurityContextHolder 객체를 스프링 시큐리티에 제공함.
    • SecurityContextHolder는 SecurityContext 객체를 Thread-local로 제공하여 같은 스레드에서는 언제든지 인증 정보에 접근할 수 있음.
package org.springframework.security.core.context;

import java.lang.reflect.Constructor;
import java.util.function.Supplier;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

public class SecurityContextHolder {
    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    private static String strategyName = System.getProperty("spring.security.strategy");
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;

    public SecurityContextHolder() {
    }

    private static void initialize() {
        initializeStrategy();
        ++initializeCount;
    }

    private static void initializeStrategy() {
        if ("MODE_PRE_INITIALIZED".equals(strategyName)) {
            Assert.state(strategy != null, "When using MODE_PRE_INITIALIZED, setContextHolderStrategy must be called with the fully constructed strategy");
        } else {
            if (!StringUtils.hasText(strategyName)) {
                strategyName = "MODE_THREADLOCAL";
            }

            if (strategyName.equals("MODE_THREADLOCAL")) {
                strategy = new ThreadLocalSecurityContextHolderStrategy();
            } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
                strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
            } else if (strategyName.equals("MODE_GLOBAL")) {
                strategy = new GlobalSecurityContextHolderStrategy();
            } else {
                try {
                    Class<?> clazz = Class.forName(strategyName);
                    Constructor<?> customStrategy = clazz.getConstructor();
                    strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
                } catch (Exception var2) {
                    ReflectionUtils.handleReflectionException(var2);
                }

            }
        }
    }

    public static void clearContext() {
        strategy.clearContext();
    }

    public static SecurityContext getContext() {
        return strategy.getContext();
    }

    public static Supplier<SecurityContext> getDeferredContext() {
        return strategy.getDeferredContext();
    }

    public static int getInitializeCount() {
        return initializeCount;
    }

    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }

    public static void setDeferredContext(Supplier<SecurityContext> deferredContext) {
        strategy.setDeferredContext(deferredContext);
    }

    public static void setStrategyName(String strategyName) {
        SecurityContextHolder.strategyName = strategyName;
        initialize();
    }

    public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
        Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
        strategyName = "MODE_PRE_INITIALIZED";
        SecurityContextHolder.strategy = strategy;
        initialize();
    }

    public static SecurityContextHolderStrategy getContextHolderStrategy() {
        return strategy;
    }

    public static SecurityContext createEmptyContext() {
        return strategy.createEmptyContext();
    }

    public String toString() {
        String var10000 = strategy.getClass().getSimpleName();
        return "SecurityContextHolder[strategy='" + var10000 + "'; initializeCount=" + initializeCount + "]";
    }

    static {
        initialize();
    }
}
  • Spring Security authentication(인증) 모델의 핵심은 SecurityContextHolder.
    • SecurityContextHolder
      • SecurityContext를 관리하는 도구로, 애플리케이션 전반에서 접근할 수 있도록 제공.
      • 여기에 SecurityContext가 포함되어 있음.
    • SecurityContext
      • 인증된 사용자의 Authentication 객체를 담는 컨테이너.
      • 기본적으로 Spring Security는 SecurityContext를 ThreadLocal에 저장함.
        • 요청마다 별도의 SecurityContext가 관리.
      • HttpSession을 통해 영속화.
        • Spring Security는 기본적으로 SecurityContext를 세션(HttpSession)에 저장하여, 요청 간 인증 정보를 유지함.
  • Principal
    • 인증된 사용자 정보.
  • Credentials
    • 인증에 사용된 정보(Ex. 비밀번호)가 포함.
  • Authorities
    • 사용자가 가진 권한(roles 혹은 권한 목록)이 포함.

접근 방법.

SecurityContextHolder.getContext().getAuthentication()
  • SecurityContextHolder의 메소드는 static으로 선언되기 때문에 어디서든 접근할 수 있음.

  • SecurityContextHolder는 SecurityContext를 관리하지만, 실제 저장 및 조회 작업은 SecurityContextHolderStrategy에 위임함.

    • SecurityContextHolder는 인증된 사용자별로 SecurityContext를 저장하고 관리하는 데 사용.
    • 멀티쓰레드 환경에서도 사용자별 인증 정보(SecurityContext)가 겹치지 않도록 보장함.
  • Spring Security의 기본 전략은 ThreadLocalSecurityContextHolderStrategy로, 쓰레드별로 SecurityContext를 저장하여 멀티쓰레드 환경에서 인증 정보가 충돌하지 않도록 함.

package org.springframework.security.core.context;

import java.util.function.Supplier;
import org.springframework.util.Assert;

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal();

    ThreadLocalSecurityContextHolderStrategy() {
    }

    public void clearContext() {
        contextHolder.remove();
    }

    public SecurityContext getContext() {
        return (SecurityContext)this.getDeferredContext().get();
    }

    public Supplier<SecurityContext> getDeferredContext() {
        Supplier<SecurityContext> result = (Supplier)contextHolder.get();
        if (result == null) {
            SecurityContext context = this.createEmptyContext();
            result = () -> {
                return context;
            };
            contextHolder.set(result);
        }

        return result;
    }

    public void setContext(SecurityContext context) {
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        contextHolder.set(() -> {
            return context;
        });
    }

    public void setDeferredContext(Supplier<SecurityContext> deferredContext) {
        Assert.notNull(deferredContext, "Only non-null Supplier instances are permitted");
        Supplier<SecurityContext> notNullDeferredContext = () -> {
            SecurityContext result = (SecurityContext)deferredContext.get();
            Assert.notNull(result, "A Supplier<SecurityContext> returned null and is not allowed.");
            return result;
        };
        contextHolder.set(notNullDeferredContext);
    }

    public SecurityContext createEmptyContext() {
        return new SecurityContextImpl();
    }
}
  • SecurityContextHolder 은 SecurityContext를 ThreadLocal에 저장.

    • 요청이 처리되는 동안에는 현재 쓰레드에 SecurityContext가 저장되어 요청 중간에 언제든 접근할 수 있음.
  • SecurityContext의 생명 주기

    • Authentication 객체를 관리하는 SecurityContext는 사용자의 요청이 서버로 들어오면 생성되고, 처리가 끝난 후 응답되는 순간에 초기화됨.
      • 요청이 끝난 후, 인증 정보를 유지해야 하는 경우(Ex. 로그인을 유지)는 SecurityContext를 HttpSession에 저장.
        • 요청이 시작될 때, HttpSession에 저장된 SecurityContext를 읽어와 SecurityContextHolder에 설정.
      • 이 역할을 하는 클래스가 HttpSessionSecurityContextRepository.
  • 접근 쓰레드별 SecurityContext 배분

    • 톰캣과 같은 WAS(Web Application Server)는 멀티쓰레드 방식으로 동작하며, 사용자가 접속하면 각 사용자에게 하나의 쓰레드가 할당됨.
    • 이러한 환경에서 각각의 사용자는 동시에 Spring Security의 인증 및 권한 로직을 수행할 수 있음.
  • Spring Security는 SecurityContextHolder를 통해 사용자별 인증 정보를 관리하는데, 이를 위해 ThreadLocal을 활용함.

    • ThreadLocal은 각 쓰레드별로 독립적인 데이터를 저장할 수 있도록 지원하므로, 쓰레드 간에 데이터가 공유되지 않음.
    • 만약 SecurityContextHolder가 ThreadLocal을 사용하지 않는다면, 모든 쓰레드가 동일한 메모리 공간(code 영역)의 SecurityContext를 참조하게 되어 사용자 인증 정보가 덮어씌워질 위험이 있음.
      • 하지만 ThreadLocal을 사용함으로써 쓰레드마다 별도의 SecurityContext를 가지게 되어 안전하게 인증 정보를 관리할 수 있음.
profile
Every cloud has a silver lining.

0개의 댓글