[Spring Security] 3. SecurityContextHolderFilter

전유림·2024년 2월 15일
0

Spring Security

목록 보기
4/8

SecurityContextHolderFilter

SecurityContextHolderFilter는 SecurityFilterChain의 Security Filter로 SecurityContext를 HTTP 요청 간에 관리하는 역할을 수행한다. 이를 위해 SecurityContextRepository를 사용한다.

해당 필터의 동작 방식은 다음과 같다.

  • SecurityContextHolderFilter는 SecurityContext를 SecurityContextRepository에서 로드하고 SecurityContextHolder에 설정한다.

주의할 점은 다음과 같다.

  • SecurityContextHolderFilter는 SecurityContext를 로드하지만 이를 SecurityContextRepository에 다시 저장하지 않는다.
  • 즉, 요청 처리 중에 SecurityContext에 대한 변경 사항이 자동으로 지속되지 않는다.
  • 따라서 SecurityContextHolderFilter를 사용할 때는 수정된 SecurityContext를 명시적으로 SecurityContextRepository에 저장해야 한다.
public class SecurityContextHolderFilter extends GenericFilterBean {

	private static final String FILTER_APPLIED = SecurityContextHolderFilter.class.getName() + ".APPLIED";

	private final SecurityContextRepository securityContextRepository;

	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
		.getContextHolderStrategy();

	/**
	 * Creates a new instance.
	 * @param securityContextRepository the repository to use. Cannot be null.
	 */
	public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
		this.securityContextRepository = securityContextRepository;
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {
		if (request.getAttribute(FILTER_APPLIED) != null) {
			chain.doFilter(request, response);
			return;
		}
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
		try {
			this.securityContextHolderStrategy.setDeferredContext(deferredContext);
			chain.doFilter(request, response);
		}
		finally {
			this.securityContextHolderStrategy.clearContext();
			request.removeAttribute(FILTER_APPLIED);
		}
	}

	/**
	 * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
	 * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
	 *
	 * @since 5.8
	 */
	public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
		Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
		this.securityContextHolderStrategy = securityContextHolderStrategy;
	}

}

  • SecurityContextRepository의 구현체 중 하나인 HttpSessionSecurityContextRepository는 세션 기반의 인증 방식에서 사용된다.
  • 사용자가 세션에 인증되어 있는 경우, HttpSessionSecurityContextRepository는 세션에 저장된 SecurityContext를 반환하여 사용자의 인증 정보를 유지한다.
  • 그러나 사용자가 세션에 인증되어 있지 않은 경우, 새로운 SecurityContext를 생성하여 반환한다.
  • SecurityContextHolderStrategy의 구현체 중 하나인 ThreadLocalSecurityContextHolderStrategy를 통해 SecurityContext를 SecurityContextHolder에 저장한다.

SecurityContextHolder

SecurityContextHolder는 Spring Security 인증 모델의 핵심으로, 현재 실행 중인 스레드의 SecurityContext를 제어한다. 이 SecurityContext에는 현재 인증된 사용자의 인증 객체가 포함되어 있다. 일반적으로 이는 Authentication 객체이다.

즉, 요청을 처리하는 동안에 인증된 사용자의 인증 정보에 접근해야 할 때마다 SecurityContextHolder를 사용하여 현재 SecurityContext를 가져올 수 있다. 이를 통해 인증된 사용자의 세부 정보와 권한을 확인할 수 있다.

  • SecurityContextHolder에 SecurityContext를 설정하는 방법
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context); 

새로운 SecurityContext 인스턴스를 생성하여 SecurityContextHolder에 설정한다 이때, 새로운 Authentication 객체를 생성하여 SecurityContext에 설정한다.

이렇게 함으로써 현재 사용자의 인증 및 권한 정보를 포함하는 SecurityContext를 해당 요청을 처리하는 동안 전역적으로 사용할 수 있다.

  • SecurityContextHolder에서 SecurityContext를 꺼내오는 방법
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

더 간편한 방법으로 현재 인증된 사용자에 대한 정보가 필요할 때 SecurityContextHolder.getContext().getAuthentication()을 호출할 수 있다.
해당 메서드는 현재 스레드의 SecurityContext에서 인증 객체를 반환한다. 따라서 이 객체를 통해 사용자의 상세 정보 및 권한을 액세스할 수 있다.

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라는 필드를 내부적으로 가지고 있다. 이 필드는 실제로 SecurityContext의 저장을 담당한다.

initialize 메서드를 SecurityContextHolder가 SecurityContext를 저장하고 관리하는 방식이 결정된다.

MODE_THREADLOCAL: 각 스레드마다 별도의 SecurityContext를 유지. 스레드 간에는 공유되지 않으며, 각 스레드에서는 독립적으로 보안 컨텍스트를 유지.
MODE_INHERITABLETHREADLOCAL: 현재 스레드의 SecurityContext가 자식 스레드에 상속. 따라서 스레드 간에는 동일한 보안 컨텍스트가 유지.
MODE_GLOBAL: 전역적으로 단 하나의 SecurityContext를 유지. 따라서 응용 프로그램 전체에서 동일한 보안 컨텍스트를 사용.
  • 일반적으로 변경이 없으면 기본적으로 ThreadLocalSecurityContextHolderStrategy가 사용된다.
  • 이 전략은 각 스레드마다 SecurityContext를 별도로 관리하며, 스레드 간에는 공유되지 않는다. 이를 통해 각 스레드가 독립적으로 보안 SecurityContext를 유지할 수 있다.
  • 이와 달리 InheritableThreadLocalSecurityContextHolderStrategy는 현재 스레드의 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();
	}

}
  • SecurityContextHolder는 주로 ThreadLocal을 사용하여 현재 스레드의 SecurityContext를 저장하고 관리한다.
  • ThreadLocal은 멀티스레드 환경에서 각 스레드마다 독립적으로 값을 유지하고 접근할 수 있도록 해주는 클래스다.
  • Thread가 수행하는 호출 스택의 어디에 있던 Thread 본인이 수행하고 있다면 어디에서든 접근할 수 있다.

SecurityContext

SecurityContext는 SecurityContextHolder에서 얻을 수 있다. SecurityContext에는 Authentication 객체가 포함되어 있다.

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 other) {
			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();
	}

}
  • SecurityContext의 실제 구현체에 해당하는 클래스로 Authentication이라는 필드를 갖고 있어 SecurityContextHolder에 Authentication을 담기 위한 목적이 있다.

Authentication

Authentication 인터페이스는 Spring Security 내에서 두 가지 주요 목적을 제공한다.

  • AuthenticationManager에게 사용자의 자격 증명을 제공하여 사용자를 인증하는 데 사용된다.
  • 현재 인증된 사용자를 나타낸다. SecurityContext에서 현재 Authentication을 얻을 수 있다.

Authentication에는 다음이 포함된다.

  • Principal: 사용자의 식별 정보를 나타낸다. 주로 UserDetails 인터페이스의 구현체이다. 아이디 및 사용자의 고유 식별자 정보를 포함한다.
  • Credentials: 사용자의 자격 증명을 나타낸다. 주로 암호 또는 인증 토큰과 같은 비밀 정보를 의미한다.
  • Authorities: 사용자에게 부여된 권한을 나타낸다. GrantedAuthority 인터페이스를 구현한 객체의 집합으로 표현된다. 주로 역할(Role)이나 스코프(scope)와 같은 것을 나타낸다.

0개의 댓글