[Spring Security] Authentication & SecurityContext

본 블로그의 모든 Spring Security 포스팅은 Spring Boot 3 이상 버전을 기준으로 작성됩니다

 

Authentication

Spring Security에서의 Authentication은 인증 및 인가 프로세스 전역적으로 사용되는 토큰의 개념이라고 생각하면 된다

  • 인증 프로세스 → 여러 인증 절차를 거친 후 AuthenticationToken을 통해서 최종적으로 인증이 되었는지 판별
  • 인가 프로세스 → 인증된 AuthenticationToken & FilterInvocation & GrantedAuthority 기반 자원에 대한 인가 처리
public interface Authentication extends Principal, Serializable {
    /**
     * Application에서 부여한 권한 정보들
     */
    Collection<? extends GrantedAuthority> getAuthorities();
    
    /**
     * Authentication Request의 메타 정보 (AuthenticationDetailsSource)
     * -> IP Address
     * -> Session ID
     * -> ...
     */
    Object getDetails();

    /**
     * Authentication 처리된 Identity Object
     * -> Principal
     * -> UserDetails
     * -> ...
     */
    Object getPrincipal();

    /**
     * AuthenticationToken을 활용한 인증 프로세스 시 사용할 자격증명
     * -> 일반적으로 계정의 비밀번호
     */
    Object getCredentials();

    /**
     * 현재 요청에 대해서 AuthenticationToken이 인증되었는지 여부
     */
    boolean isAuthenticated();

    /**
     * 인증 여부 설정
     */
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

 

간략한 인증 프로세스

  • 인증 프로세스가 진행되는 Basic한 흐름

  • 가장 기본적인 Form Login Based UsernamePasswordAuthenticationFilter
  • isAuthenticated = false인 UsernamePasswordAuthenticationToken을 통해서 인증 프로세스 진행

  • 인증 완료 후 successfulAuthentication 로직 흐름
  • 인증된 AuthenticationToken을 SecurityContext로 감싸고 SecurityContext를 SecurityContextHolder에 담아두기

 

SecurityContext & SecurityContextHolder

SecurityContext → 인증된 AuthenticationToken을 감싸는 상자 개념
SecurityContextHolder → SecurityContext를 Strategy에 따라 담아두는 Holder

// SecurityContext
public interface SecurityContext extends Serializable {
	Authentication getAuthentication();

	void setAuthentication(Authentication authentication);
}

// SecurityContextHolder
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(SYSTEM_PROPERTY);
	private static SecurityContextHolderStrategy strategy;
	private static int initializeCount = 0;

	static {
		initialize();
	}

	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");
			return;
		}
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}
		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
			return;
		}
		// Try to load a custom strategy
		try {
			Class<?> clazz = Class.forName(strategyName);
			Constructor<?> customStrategy = clazz.getConstructor();
			strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
		}
		catch (Exception ex) {
			ReflectionUtils.handleReflectionException(ex);
		}
	}

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

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

 

이렇게 Strategy에 따라 SecurityContext를 SecurityContextHolder에 담아두고 securityContextHolderStrategy.getContext()를 통해서 필요한 어느곳에서든 꺼내서 활용할 수 있는 것이다

 

3가지 SecurityContextHolder 전략

1. MODE_THREADLOCAL

각각의 요청에 대한 SecurityContext를 ThreadLocal로 관리하는 방식

 

2. MODE_INHERITABLETHREADLOCAL

Parent Thread & Created Child Thread간 InheritableThreadLocal을 활용해서 관리하는 방식

 

3. MODE_GLOBAL

전역적으로 1개의 SecurityContext를 관리하고 공용으로 활용하는 방식

 

 

SecurityContext를 관리하는 방식 (SecurityContextRepository)

1. HttpSessionSecurityContextRepository

HttpSession에 SecurityContext를 저장하는 방식

 

2. NullSecurityContextRepository

SecurityContext를 어디에도 저장하지 않는 방식

  • 어디에도 SecurityContext를 저장하지 않는 방식이기 때문에 saveContext의 로직은 비어있고 loadContext또한 저장이 안되어 있기 때문에 emptyContext를 받아오게 된다

 

SessionManagement → SessionCreationPolicy

어떤 상황에서 HttpSession에 저장하고 어떤 상황에서 NullRepository를 통해서 SecurityContext를 저장하지 않는지는 SecurityFilterChain에서 SecurityConfig를 진행할 때 SessionManagement의 SessionCreationPolicy를 어떻게 설정하냐에 따라 다르다

 

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        // ...
        .sessionManagement((session) -> session
            .sessionCreationPolicy(SessionCreationPolicy.{Specific Policy...})
        );
    return http.build();
}
  1. ALWAYS → 항상 HttpSession 활용
  2. NEVER → HttpSession이 없으면 굳이 만들지는 않지만 있으면 있는거 활용
  3. IF_REQUIRED → 필요하면 HttpSession 활용
  4. STATELESS → HttpSession을 절대 사용 X
1 ~ 3 = HttpSessionSecurityContextRepository
4 = NullSecurityContextRepository

  • 어떠한 설정도 없는 경우 기본값 = IF_REQUIRED

 

SecurityContextHolderFilter

FilterChainProxy에서 Chaining되는 여러 필터중에 꽤 앞단에 위치한 SecurityContextHolderFilter의 역할 및 로직을 알아보자

 

SecurityContextHolderFilter는 SecurityContextRepository를 통해서
→ 1. 요청이 들어오는 시점에 SecurityContextHolder에 SecurityContext를 load해서 넣어주기
→ 2. 요청이 마무리되는 시점에 SecurityConextHolder 비우기