[Spring Security] Spring Security Basic - Security Context Holder

Updated:

들어가며

Spring Security에서 사용되는 SecurityContextHolder가 하는 역할과 언제 생성되고, 어떻게 데이터가 들어가는지 알아 보자.

역할

SecurityContextHolder의 역할을 정말 간단하다.

  • ThreadLocal을 사용해서, request당 Authentication 정보 담고 있는 Holder이다.
  • Authentication은 인증이 되어야만 생성되기에 SecurityContextHolder가 담고 있는 값을 확인하여 인증이 되었는지, 안되었는지 알 수 있다.

SecurityContextHolder

public class SecurityContextHolder {
	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++;
	}
    ...
}
  • SecurityContextHolder는 static method를 통해서 서버 기동 시점에 어떤 방식의 전략으로 생성될 지 결정되어 진다.
  • SpringSecurity의 default는 MODE_THREADLOCAL이며 Custom도 가능하다.
  • 만약 값을 변경하고 싶다면, SystemPropertyspirng.security.strategy를 변경해서 사용하면 된다.
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);
    ... 
}

MODE_THREADLOCAL

  • 해당 Thread에서만 사용가능한 SecurityContext를 가지고 있는 모드 이다.

MODE_INHERITABLETHREADLOCAL

  • ThreadLocal에 있는 정보를 하위 Thread에 SecurityContext를 전파 하는 모드 이다.

MODE_GLOBAL

  • 해당 application에서 모든 Thread에 같은 SecurityContext를 갖게 하는 모드 이다.

SecurityContext

public interface SecurityContext extends Serializable {	
	Authentication getAuthentication();

	void setAuthentication(Authentication authentication);
}
  • SecurityContextHolder가 Authentication을 갖고 있다고 했는데, 실제로는 SecurityContext를 갖고 있고 SecurityContext를 통해서 Authentication을 가져올 수 있다.
  • SecurityContext의 구현체는 SecurityContextImplAuthentication을 갖고 있다.
public class SecurityContextImpl implements SecurityContext {
    ...
	private Authentication authentication;

	public SecurityContextImpl() {}

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

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

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

Authentication

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은 사용자로부터 전달받은 데이터를 보관 및 가공하여 인증정보를 갖고 있는 인터페이스이다.
  • 실제로 데이터를 갖고 있는 class는 XXXToken으로 끝나는 class들이다.
    • UsernamePasswordAuthenticationToken
    • AnonymousAuthenticationToken
  • GrandedAuthority는 해당 유저의 Admin, User와 같은 권한을 체크하는데 사용 된다.
  • Credentials는 password 정보를 담고 있는데, 인증이 완료되면 해당 데이터를 제거한다.
  • Details는 정확하게는 모르겠지만, remoteAddresssessionId를 갖고 있다..
  • Principal 인증에서 가장 중요한 데이터라 생각되는데, 유저 정보를 담고 있는 데이터 이다.
    • Object로 되어 있지만, 앞에서 얘기했던 Token class 값이다.
    • username, authorities, 유효성 체크할 수 있는 필드에 데이터를 갖고 있다.

SecurityContextHolder 에 데이터 주입 과정

  • SecurityContextHolder에 어떤 데이터가 들어가는지는 알아 보았으니, 언제 데이터가 담기는지 확인해 보자.

SecurityContextPersistenceFilter

public class SecurityContextPersistenceFilter extends GenericFilterBean {
    ...
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		...
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
				response);
		SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

		try {
			SecurityContextHolder.setContext(contextBeforeChainExecution);

			chain.doFilter(holder.getRequest(), holder.getResponse());

		}
		finally {
			SecurityContext contextAfterChainExecution = SecurityContextHolder
					.getContext();
			// Crucial removal of SecurityContextHolder contents - do this before anything
			// else.
			SecurityContextHolder.clearContext();
			repo.saveContext(contextAfterChainExecution, holder.getRequest(),
					holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);

			if (debug) {
				logger.debug("SecurityContextHolder now cleared, as request processing completed");
			}
		}
	}
    ...
}
  • SecurityContextHolderSecurityContextSecurityContextPersistenceFilter에서 doFilter가 동작될 때 생성 된다.
  • session 방식을 이용 한다면, finally 부분에서 Session에 SecurityContext를 저장하고 session에서 SecurityContext를 load하여 셋팅하는 부분도 포함되어 있다.
  • SecurityContext가 생성되었다면, SecurityContextAuthenticatioon은 언제 생성되는지 알아 보자.

AbstractAuthenticationProcessingFilter

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
		implements ApplicationEventPublisherAware, MessageSourceAware {
	...
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		...
		Authentication authResult;

		try {
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		...

		successfulAuthentication(request, response, chain, authResult);
	}
    ...
	protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		...
		SecurityContextHolder.getContext().setAuthentication(authResult);
		...
	}
    ...
}
  • AbstractAuthenticationProcessingFilterattemptAuthentication 메소드 뜻 그대로 인증을 시도 하는 filter이다.
  • authResult가 Authenticatioon 데이터를 담고 있고, doFilter 마지막에 성공인 경우 successfulAuthentication에서 SecurityContextHolderauthResult를 저장한다.
  • 이번에는 실제로 authResult를 만드는 authentication을 처리하는 곳은 어디인지 알아보자.

AuthenticationManager

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
  • Authentication을 받아서 Authentication을 제공하는 인터페이스이다.
  • 파라메터로 받은 Authentication은 사용자로부터 입력받은 유저 정보이고, return 값은 인증을 완료한 유저 데이터를 전달 한다.
  • 인터페이스로 되어 있으니, 실제로 구현 class는 무엇인지 알아 보자.

ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {
	...
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		...
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}
        ...

		throw lastException;
	}
}
  • ProviderManagerList<AuthenticationProvider>를 갖고 있어, 다른 여러 Provider 중에서 인증에 성공 한걸로 인증을 완료한다.
  • AuthenticationProvider의 default로는 AnonymousAuthenticationProvider를 갖고 있는데 해당 부분은 조건을 충족하지 못해 reulst == null 되고, AnonymousAuthenticationProvider는 parent를 갖고 있는데, parent는 DaoAuthenticationProvider이다.
  • 따라서 DaoAuthenticationProviderauthenticate가 실행되게 된다.

DaoAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
	...
	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}
}
  • DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider를 상속 받아서 retrieveUser만 구현되어 있다.
  • retrieveUserUserDetailsService를 통해서 UserDetails를 가져오는데, UserDetails가 인증된 유저의 데이터이다.
  • UserDetailsService를 통해서 인증 관련 UserDetails 데이터를 가져오는건 이전 포스트에서 확인하였다.

AbstractUserDetailsAuthenticationProvider

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		...

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}
        ...
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
}
  • AbstractUserDetailsAuthenticationProviderAuthenticationProvider를 상속 받고 있다.
  • 따라서 해당 class에 authenticate이 실행되고 retrieveUserDaoAuthenticationProvider에 있는 메소드를 사용하게 된다.
  • 다시 역순으로 돌아가서, AuthenticationManager를 통해서 SecurityContextHolder에 필요한 Authentication을 얻을 수 있었다.
  • Authentication을 얻기까지 많은 Filter를 거치게 되는데, SpringSecurity에서는 어떤식으로 FilterChain이 이뤄지는지는 다음 포스트에서 알아보자.

마치며

  • 인증이 처리되는 과정이 하나씩 쫓아가면서 확인하였다.. 직접 디버깅을 해가면서 따라가지 않으면 아마 금방 까먹고 말 것이니, 실제로 break point를 설정하고 디버깅을 통해서 직접 확인해보는게 좋아보인다!
  • form 방식의 기본 설정만 추가하여 테스트를 진행하였기에, 다른 설정의 경우는 다르게 적용 될 수 있다
  • 하지만, 동작 방식을 알았으니, 디버깅을 통해서 확인을 해볼때 어느정도 쉽게 유추가 되지 않을까 싶다!

Leave a comment