1. 스프링 시큐리티 인증 아키텍처
SecurityContextHolder
SecurityContextHolder
는 스프링 시큐리티 인증 모델의 핵심입니다.
이것은 인증된 사용자에 대한 세부 정보(details)를 저장하는 곳입니다. 어떤 값이 포함되어 있든 간에 값이 포함되어 있기만 하면 인증된 사용자로 간주됩니다.
SecurityContextHolder
는 전략 패턴을 사용합니다. SecurityContextHolderStrategy
를 클래스 변수로 가지며 ThreadLocal
을 사용하여 세부 정보를 저장하는 ThreadLocalSecurityContextHolderStrategy
을 기본 전략으로 사용합니다.
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal<>();
...
}
ThreadLocal
을 사용함으로써 동일한 쓰레드에서는 항상 SecurityContext
를 사용할 수 있습니다.
ThreadLocal
은 의도하지 않은 값의 참조가 발생하지 않도록 쓰레드가 쓰레드풀에 반환되기 전 변수 값을 반드시 제거해야 합니다. 그리고 스프링 시큐리티의 FilterChainProxy
는 이러한 SecurityContext
가 지워지는 것을 보장합니다.
FilterChainProxy
의 doFilter
메서드의 finally 부분에서 securityContextHolderStrategy
의 clearContext
를 호출하는 것을 확인할 수 있습니다.
public class FilterChainProxy extends GenericFilterBean {
...
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
doFilterInternal(request, response, chain);
return;
}
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
} catch (Exception ex) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
Throwable requestRejectedException = this.throwableAnalyzer
.getFirstThrowableOfType(RequestRejectedException.class, causeChain);
if (!(requestRejectedException instanceof RequestRejectedException)) {
throw ex;
}
this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response,
(RequestRejectedException) requestRejectedException);
} finally {
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
...
}
SecurityContext
SecurityContext
는 SecurityContextHolder
로부터 얻을 수 있습니다.
public class SecurityContextHolder {
...
public static SecurityContext getContext() {
return strategy.getContext();
}
...
}
SecurityContext
자체는 어떤 특별할 기능도 제공하지 않습니다. 단순히 Authentication
객체를 포함하고 있을 뿐입니다.
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
Authentication
Authentication
인터페이스는 다음 세 가지를 포함합니다.
principal
: 사용자를 식별합니다.credentials
: 비밀번호입니다. 사용자가 인증된 후에는 유출되지 않도록 지워집니다.authorities
:GrantedAuthority
인스턴스로 사용자에게 부여되는 상위 수준의 권한입니다.
Authentication
의 구현체는 Token
이라는 접미사가 붙으며 대표적으로 다음과 같은 것들이 있습니다.
AnonymousAuthenticationToken
: 익명 사용자를 표현하기 구현체입니다.UsernamePasswordAuthenticationToken
: 로그인 아이디/비밀번호를 표현하기 위한 구현체입니다.RememberMeAuthenticationToken
: remember-me 기반 구현체입니다.
2. 인증 프로세스
AbstractAuthenticationProcessingFilter
사용자 인증을 처리하기 위한 필터입니다. 구현체로 UsernamePasswordAuthenticationFilter
가 있습니다.
사용자의 인증을 위한 정보(credentials)를 취합하고 Authentication
객체를 생성합니다. 구현체에서는 로그인 아이디/비밀번호를 취합하고, Authentication
인터페이스의 구현체 중 하나인 UsernamePasswordAuthenticationToken
객체를 생성합니다.
HTTP 요청으로부터 파라미터를 뽑아내어 만들어진 Token
은 최초 인증되지 않은 상태이며, GrantedAuthority
목록도 비어있는 상태입니다.
이는 AuthenticationManager
객체로 전달되어 인증 프로세스를 수행합니다.
인증이 정상적으로 완료된다면 새롭게 만들어진 Authentication
객체를 반환합니다. 이는 인증이 완료된 상태로 표현되며 GrantedAuthority
목록을 포함하고 있습니다.
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
...
}
AuthenticationManager
AuthenticationManager
는 스프링 시큐리티의 필터가 사용자 인증을 수행하기 위한 API를 제공합니다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
일반적인 구현체로는 ProviderManager
가 있습니다.
ProviderManager
ProviderManager
는 AuthenticationProvider
목록에 인증 처리를 위임합니다. 각각의 AuthenticationProvider
는 인증이 성공, 실패, 결정할 수 없음을 알리고 다음에 오는 AuthenticationProvider
가 결정할 기회를 넘겨줍니다.
1개 이상의 AuthenticationProvider
구현체 중 어떤 구현체가 실제 인증을 처리할지 결정할 수 있습니다. 예를 들어 supports
메서드가 true
를 반환하는 AuthenticationProvider
객체가 인증을 처리하게 되는데 UsernamePasswordAuthenticationToken
의 경우 DaoAuthenticationProvider
가 인증을 처리하게 됩니다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
private List<AuthenticationProvider> providers = Collections.emptyList();
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
Authentication result = null;
...
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
...
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
} catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
} catch (AuthenticationException ex) {
lastException = ex;
}
}
...
}
...
}
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
또한 ProviderManager
에 주어진 Authentication
객체에 대한 인증을 수행할 수 있는AuthenticationProvider
가 없는 경우, 참조하고 있는 부모 AuthenticationManager
(일반적으로 ProviderManager
인스턴스)에게 인증 처리를 위임할 수 있습니다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
private AuthenticationManager parent;
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
Authentication result = null;
Authentication parentResult = null;
...
for (AuthenticationProvider provider : getProviders()) {
...
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException ex) {
// 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 ex) {
parentException = ex;
lastException = ex;
}
}
...
}
}
만일 자신이 가지고 있는 AuthenticationProvider
목록을 돌아 인증을 완료했거나, 참고하고 있는 부모의 AuthenticationProvider
목록까지 다 돌고나서 인증을 완료한 Authentication
객체를 가지고 있다면 credential 을 지우는 작업을 수행합니다. 이후 인증되었고, 권한 목록을 가지고 있는 Authentication
객체를 반환하게 됩니다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
for (AuthenticationProvider provider : getProviders()) {
...
}
if (result == null && this.parent != null) {
// Allow the parent to try.
...
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
}
}
만일 인증에 실패한다면 AuthenticationException
을 던지는 것으로 해당 로직이 종료됩니다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
AuthenticationException lastException = null;
AuthenticationException parentException = null;
...
for (AuthenticationProvider provider : getProviders()) {
...
}
if (result != null) {
...
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
}
참조
'공부방' 카테고리의 다른 글
SOLID 원칙 (0) | 2024.03.23 |
---|---|
CORS는 왜 필요할까? (0) | 2024.03.17 |
[TIL] 스프링 시큐리티 아키텍처 (0) | 2023.08.05 |
[TIL] 스프링 시큐리티 Quick Start (0) | 2023.08.05 |
[TIL 07/05] Spring MVC, REST API (0) | 2023.07.07 |