Spring/시큐리티 기본원리

시큐리티 기본 원리( 5 ) - SecurityContextHolder( 인증 정보 공유 방식 )

_Jin_ 2024. 6. 15.

SecurityFilterChain 내부에 존재하는 많은 필터로 시큐리티 관련 작업을 진행한다.

모든 작업은 필터마다 기능 단위로 분업하여 진행하기에 앞에서의 작업이 이후 필터가 알기 위한 저장소 개념이 필요하다.

 

단편적인 예시로 인가 필터가 작업을 하려면 유저의 role 정보가 필요하게 되는데,

앞단의 필터에서 유저에게 role값을 부여한 결과를 인가 필터에 도달하기 까지 공유해야 확인할 수 있다.

 

시큐리티의 정보 공유 방식

 

각 필터마다 처리된 상태를 알기 위해서는 저장소 개념이 필요하다.

 

즉, 인가 필터 이전에 아이디, 로그인 여부, role 데이터 등의 정보를 저장하고 있을 곳이 필요한데,

이러한 정보는 ‘Authentication 객체’가 저장한다

 

 

Authentication 객체가 담고 있는 정보는 크게 아래와 같다. 

Principal : 유저에 대한 정보
Credentials : 증명 (비밀번호, 토큰)
Authorities : 유저의 권한(ROLE) 목록

Authentication 객체는 SecurityContext에 포함과 관리되며, SecurityContext는 유저 정보 당 1개씩 생성된다.

그리고 이 N개의 SecurityContext는 하나의 SecurityContextHolder에 의해서 관리된다.

 

따라서 Authentication 객체가 지닌 정보에 접근하고 싶다면, 아래와 같이 가능하다.

SecurityContextHolder.getContext().getAuthentication().getAuthorities();

 

SecurityContextHolder의 메소드는 static 으로 선언되기 때문에 어디서든 접근할 수 있다.

 

 

 시큐리티의 인증 정보 부여 방법

일반적으로 다수의 사용자인 멀티 쓰레드 환경에서 시큐리티는 각 사용자마다 별도의 SecurityContext를 주어야한다.

사용자별로 다른 저장소를 제공해야 인증 정보가 겹치는 일이 발생하지 않기 때문이다.  

그리고 시큐리티에서 SecurityContext를 부여하는 기능은 다른 클래스에게 위임한다. 

 

즉, SecurityContextHolder는 다수의 SecurityContext들을 관리하는 역할로 메소드를 제공하지만,

실제로 등록, 초기화, 읽기와 같은 작업의 구현은 SecurityContextHolderStrategy라는 인터페이스를 활용한다.

 

SecurityContextHolderStrategy  구현 종류

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

위의 코드를 보면, ThreadLocal, InheritableThreadLocal, Global 등 다양한 구현체가 존재함을 알 수 있다.

그리고 기본적으로 threadlocal 방식이 사용된다.

 

이 기본 구현체의 코드는 아래와 같은 방법으로 고유한 사용자의 정보를 가지고 관리한다. 

 

ThreadLocal 방식에서 SecurityContext

ThreadLocalSecurityContextHolderStrategy

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal<>();

}

 

 

접근 쓰레드별 SecurityContext 배분

시큐리티는 WAS 단계에서 적용되며 톰캣 WAS는 멀티 쓰레드 방식으로 동작한다.

유저가 접속하면 유저에게 하나의 쓰레드를 할당한다.

그리고 각각의 유저는 동시에 시큐리티 로그인 로직을 사용할 수 있으며, 동시 접근의 경우 정보가 겹치면 안된다. 

 

따라서 하나의 SecurityContextHolder의 필드에 선언된 SecurityContext를 호출에 따른 데이터 공유를 막기위해서

threadLocal로 관리하여 쓰레드별 다른 구획을 나눠 제공한다.

 

SecurityContext의 생명 주기

Authentication 객체를 관리하는 SecurityContext는 사용자의 요청이 서버로 들어오면 생성되고, 처리가 끝난 후 응답되는 순간에 초기화 된다.

 

요약

SecurityFilterChain의 각각의 필터에서 수행한 작업 내용이 전달되기 위해 요청(유저) 별로Authentication 객체를 할당하여 확인함.

Authentication 객체는 SecurityContextHolder의 SecurityContext가 관리함.

멀티 쓰레드 환경에서 SecurityContext를 만들고 필드의 static 영역에 선언된 SecutiyContext를 다루는 전략은 기본적으로 threadLocal 전략을 이용함.

 

예시

로그인 필터 : 인증을 완료한 뒤 유저 정보를 담은 Authentication 객체를 넣음
로그아웃 필터 : 로그아웃 로직을 수행하면서 SecurityContext의 Authentication 객체를 비움

댓글