Spring Security - 3. SecurityContextPersistenceFilter 알아보기

INCHEOL'S·2021년 3월 6일
2

spring-security

목록 보기
3/5

안녕하세요. INCHEOL'S 입니다. 오늘은 Spring Security Filter 들중 SecurityContextPersistenceFilter에 대해 알아보겠습니다.

  1. SecurityContextPersistenceFilter
  2. SecurityContextHolder
  3. SecurityContext

1. SecurityContextPersistenceFilter

SecurityContextPersistenceFilter는 SecurityFilterChain에 걸리는 Filter 입니다. 클래스 이름에서 추측해 볼 수 있듯이 SecurityContext라는 녀석을 영속화 하는 책임을 가진 필터입니다.

SecurityContext의 영속화는 SecurityContextRepository라는 레포지터리를 통해 이루어지는데요. 별도의 변경이 없다면, HttpSessionSecurityContextRepository이 사용되며 HttpSession 의 Attribute에 SecurityContext라는 녀석이 저장됩니다.

이와 같이 SecurityContext를 영속화 할 뿐만 아니라, 이 녀석은 요청의 세션에서 저장되어 있던 SecurityContext를 꺼내와 SecurityContextHolder라는 홀더에 집어넣어 요청 전반에 걸쳐 SecurityContext를 사용할 수 있게끔 해줍니다.

그렇다면 SecurityContextHolder라는 녀석은 무엇이며 요청 전반에 걸쳐 어떻게 우리가 SecurityContext에 접근할 수 있도록 해주는 것일까요?

2. SecurityContextHolder

SecurityContextHolder는 Holder라는 단어에서 알 수 있듯이 (컵홀더처럼) SecurityContext전용의 '홀더' 입니다.

레퍼런스에서는 아래 그림과 함께 다음과 같이 설명합니다.

At the heart of Spring Security’s authentication model is the SecurityContextHolder. It contains the SecurityContext.

The SecurityContextHolder is where Spring Security stores the details of who is authenticated. Spring Security does not care how the SecurityContextHolder is populated. If it contains a value, then it is used as the currently authenticated user.

SecurityContextHolder는 스프링 시큐리티 인증 모델의 핵심이라고 하니 SecurityContextHolder가 어떤 책임을 수행하고 있는지 확실히 알아야 좋겠네요.

이 홀더가 갖고 있는 SecurityContext는 위 그림을 보시다시피 인증 객체를 갖고 있습니다. 이미 Spring Security를 접해보신분은 잘 알고 계시겠지만, 사용자가 인증에 성공하면 Authentication이라는 객체가 만들어집니다.(일반적으로 우리는 UsernamePasswordAuthentication을 사용합니다.)
바로 인증에 성공한 인증객체가 SecurityContext에 저장되는데요. 이 인증객체를 요청 전반에 걸쳐 사용하기 위해 SecurityContext와 그것을 담는 홀더가 생긴 것입니다.

그렇다면 어떻게 요청 전반에 걸쳐 이러한 인증객체를 전달할 수 있을까요. Spring Security에서는 ThreadLocal을 사용하였습니다. 만약 스레드 스콥을 사용하지 않고 메서드의 파라미터로 넘긴다고 생각해보시면.. 이는 애초에 불가능한 것이라는 것을 깨닫게 됩니다. 왜냐하면 요청 전반에 걸쳐 사용되는 모든 인터페이스를 바꿔야하기 때문입니다.

그럼 SecurityContextHolder가 어떻게 생겨먹었고 ThreadLocal 어떻게 사용하는지 보시겠습니다.

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";

	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() {
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			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 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);
			}
		}
		initializeCount++;
	}

	/**
	 * Explicitly clears the context value from the current thread.
	 */
	public static void clearContext() {
		strategy.clearContext();
	}

	/**
	 * Obtain the current <code>SecurityContext</code>.
	 * @return the security context (never <code>null</code>)
	 */
	public static SecurityContext getContext() {
		return strategy.getContext();
	}

	/**
	 * Primarily for troubleshooting purposes, this method shows how many times the class
	 * has re-initialized its <code>SecurityContextHolderStrategy</code>.
	 * @return the count (should be one unless you've called
	 * {@link #setStrategyName(String)} to switch to an alternate strategy.
	 */
	public static int getInitializeCount() {
		return initializeCount;
	}

	/**
	 * Associates a new <code>SecurityContext</code> with the current thread of execution.
	 * @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
	 */
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

	/**
	 * Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
	 * a given JVM, as it will re-initialize the strategy and adversely affect any
	 * existing threads using the old strategy.
	 * @param strategyName the fully qualified class name of the strategy that should be
	 * used.
	 */
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}

	/**
	 * Allows retrieval of the context strategy. See SEC-1188.
	 * @return the configured strategy for storing the security context.
	 */
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
		return strategy;
	}

	/**
	 * Delegates the creation of a new, empty context to the configured strategy.
	 */
	public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}

	@Override
	public String toString() {
		return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount=" + initializeCount + "]";
	}

}

SecurityContextHolder는 또 내부에 SecurityContextHolderStrategy라는 필드를 갖고 있는데요. 실제로 홀더 저장에 대한 책임을 이 녀석이 수행합니다. 이 녀석은 인터페이스로서 구체화 클래스는 기본으로 3가지 정도로 제공되며, initialize 메서드를 보시면 strategy 이름에 따라 사용되는 홀더 전략이 달라집니다. 일반적으로 별도의 변경이 없다면 디폴트는 ThreadLocalSecurityContextHolderStrategy이 사용됩니다. 이 녀석이 바로 ThreadLocal을 사용하여 SecurityContext를 담는데요.

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

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

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

	@Override
	public SecurityContext getContext() {
		SecurityContext ctx = contextHolder.get();
		if (ctx == null) {
			ctx = createEmptyContext();
			contextHolder.set(ctx);
		}
		return ctx;
	}

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

	@Override
	public SecurityContext createEmptyContext() {
		return new SecurityContextImpl();
	}

}

클래스가 생겨먹은 것 자체는 굉장히 심플하네요. 핵심은 ThreadLocal입니다. 해당 기능을 이용하면 Thread가 수행하는 호출 스택이 어디에 있던 수행하는 녀석이 Thread 본인이라면 어디에서든 접근할 수 있습니다. 이것 덕분에 우리는 간편하게 SecurityContext의 인증 객체에 접근 가능합니다.
Controller에서든 Service에서든 아니면 빈이 아닌 util 클래스를 작성하여 그안에서 SecurityContextHolder.getContext().getAuthentication()만 호출한다면 인증된 사용자의 정보를 얻을 수 있습니다.
(ThreadLocal의 개념을 모르시는 분이라면 한번 찾아보시고 코드를 다시 보시는것을 추천합니다.)

3. SecurityContext

마지막으로 SecurityContext입니다.
바로 이녀석의 실제 구현체인 SecurityContextImpl의 코드를 바로 보시겠습니다.

public class SecurityContextImpl implements SecurityContext {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private Authentication authentication;

	public SecurityContextImpl() {
	}

	public SecurityContextImpl(Authentication authentication) {
		this.authentication = authentication;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof SecurityContextImpl) {
			SecurityContextImpl other = (SecurityContextImpl) obj;
			if ((this.getAuthentication() == null) && (other.getAuthentication() == null)) {
				return true;
			}
			if ((this.getAuthentication() != null) && (other.getAuthentication() != null)
					&& this.getAuthentication().equals(other.getAuthentication())) {
				return true;
			}
		}
		return false;
	}

	@Override
	public Authentication getAuthentication() {
		return this.authentication;
	}

	@Override
	public int hashCode() {
		return ObjectUtils.nullSafeHashCode(this.authentication);
	}

	@Override
	public void setAuthentication(Authentication authentication) {
		this.authentication = authentication;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(getClass().getSimpleName()).append(" [");
		if (this.authentication == null) {
			sb.append("Null authentication");
		}
		else {
			sb.append("Authentication=").append(this.authentication);
		}
		sb.append("]");
		return sb.toString();
	}

정말 단순히 Authentication이라는 필드를 갖고 있고 게터,세터를 제공하는군요. SecurityContextHolder에 Authentication을 담기 위해 SecurityContext로 래핑해 놓았을 뿐이네요..ㅎ

이것으로 오늘 포스트는 마치겠습니다.

정리.
1. SecurityContextPersistenceFilter는 SecurityContext(Authentication)을 영속화 하는 것과 요청 세션에서 SecurityContext를 갖고와 요청 전반에 걸쳐 인증된 사용자에 대한 정보를 접근하기 쉽게 SecurityContextHolder에 집어 넣어준다.

  1. SecurityContextHolder는 SecurityContext를 담는 역할을 하고 담는 방법은 기본으로 3가지가 제공되며 디폴트는 ThreadLocal에 담기도록 한다. 이는 하나의 요청을 하나의 스레드로 처리하는 서블릿 기반 컨테이너에서 SecurityContext에 대한 접근성을 쉽게 만들어준다.

  2. SecurityContext는 Authentication를 래핑해 놓았을 뿐...

profile
제주하르방백년초콜릿 먹고싶네요. 아, 저는 백엔드 개발자입니다.

1개의 댓글

comment-user-thumbnail
2023년 4월 19일

좋은 글 감사합니다!!

답글 달기