*개요
Authentication(인증) 아키텍처 공식문서를 확인합니다.
자세한 내용은 Servlet Authentication Architecture에서 확인 가능합니다.(5.6.13-SNAPSHOT 기준)
SecurityContextHolder
스프링 시큐리티 인증 모델의 핵심은 SecurityContextHolder입니다. 인증 정보들을 저장하는 장소입니다. 스프링 시큐리티는 SecurityContextHolder가 어떻게 채워지는지 관심이 없습니다. 값이 있다면, 인증된 사용자로 활용합니다. 즉, 사용자 인증을 구현하려면 SecurityContextHolder에 직접 인증정보를 넣습니다.
SecurityContext context = SecurityContextHolder.createEmptyContext(); // (1)
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER"); // (2)
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context); // (3)
(1) SecurityContext를 생성합니다. SecurityContextHolder.getContext().setAuthentication(authentication) 대신 SecurityContext를 생성해야 멀티 쓰레드 환경에서 경쟁 상태를 예방할 수 있습니다.
(2) Authentication 객체를 생성합니다. 스프링 시큐리티는 어떤 종류의 Authentication 구현체가 SecurityContext에 설정되는지 관심이 없습니다. 단순한 TestingAuthenticationToken을 사용하며 운영환경에서는 UsernamePasswordAuthenticationToken(userDetails, password, authorities)을 더 많이 사용합니다.
(3) 마지막으로 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는 ThreadLocal을 사용해 정보를 저장합니다. 즉, SecurityContext가 명시적으로 메서드의 인자로 전달되지 않더라도 동일 쓰레드라면 메서드를 항상 사용할 수 있습니다.(getContext(), getAuthentication(), getName(), getPrincipal() 등등) 만약 현재 인증 요청이 수행된 이후 쓰레드를 초기화하고 싶다면 ThreadLocal 사용이 안전합니다. FilterChainProxy는 SecurityContext가 항상 초기화됨을 보장합니다.
SecurityContext
SecurityContext는 SecurityContextHolder에서 획득합니다. SecurityContext는 Authentication 객체를 가지고 있습니다.
Authentication은 2가지 중요한 역할을 합니다.
- 사용자가 인증하기 위해 제공한 credentials을 AuthenticationManager에 넣습니다.
- 현재 인증된 사용자를 대표합니다. 현재 Authentication은 SecurityContext에서 획득할 수 있습니다.
Authentication은 3가지를 가지고 있습니다.
1. principal : 사용자 식별정보로 아이디/비밀번호로 로그인하면 UserDetails의 인스턴스입니다.
2. credentials : 종종 비밀번호로 사용되며 많은 경우 사용자가 인증을 끝내면 삭제됩니다.
3. authorities : GrantedAuthority는 사용자가 부여받는 높은 수준의 권한입니다. roles와 scopes 등이 있습니다.
GrantedAuthority
GrantedAuthority는 Authentication.getAuthorities() 메서드로 획득할 수 있습니다. 이 메서드는 GrantedAuthority 객체들의 Collection을 리턴합니다. GrantedAuthority는 principal에 부여된 권한입니다. 이 권한들은 ROLE_ADMINISTRATOR 혹은 ROLE_HR_SUPERVISOR와 같은 "역할"을 가집니다. 이런 역할들은 웹 인가, 메서드 인가, 도메인 객체 인가를 위해 나중에 설정됩니다. 스프링 시큐리티는 이러한 권한들을 해석할 수 있고, 해당 권한이 있기를 기대합니다. 아이디/비밀번호 기반 인증을 사용하면 GrantedAuthority는 보통 UserDetailsService에 의해 조회됩니다.
GrantedAuthority 객체는 어플리케이션 전체 권한입니다. 특정 도메인 객체에만 한정되지 않습니다. 예를 들어, Employee 54번째 객체만의 권한을 나타내는 GrantedAuthority가 없을 가능성이 높습니다. 만약에 그러한 권한들을 수천 개 가지고 있다면 메모리를 엄청나게 사용하기 때문입니다. 물론, 스프링 시큐리티는 이러한 공통 요청을 처리하도록 설계되었지만 대신 프로젝트의 도메인 객체 보안 기능을 사용하게 됩니다.
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
AuthenticationManager
AuthenticationManager는 스프링 시큐리티 필터가 어떻게 인증을 수행할지 정의하는 API입니다. AuthenticationManager가 호출한 컨트롤러(스프링 시큐리티 Filters)는 반환된 Authentication를 SecurityContextHolder에 설정합니다. 만약 스프링 시큐리티의 Filter를 통합하지 않는다면 SecurityContextHolder을 직접 설정할 수도 있고 AuthenticationManager를 사용하지 않을 수 있습니다.
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
ProviderManager
ProviderManager는 AuthenticationManager의 가장 일반적인 구현체입니다. ProviderManager는 AuthenticationProviders의 List에 인증 검증을 위임합니다. 각각 AuthenticationProvider는 인증이 성공인지, 실패인지, 결정할 수 없는지 확인하며 다음순서(아래방향) AuthenticationProvider에게 결정을 넘길 수도 있습니다. 어떠한 AuthenticationProvider도 인증할 수 없다면 ProviderNotFoundException을 던집니다. 즉, 통과하는 어떠한 Authentication도 ProviderManager이 지원하지 못하는 경우로 AuthenticationException 중 하나입니다.
각각 AuthenticationProvider는 특정 종류의 인증을 수행합니다. 예를 들어, 어떤 AuthenticationProvider는 아이디/비밀번호 검증을 할 수 있으며, 다른 AuthenticationProvider는 SAML 인증을 수행합니다. AuthenticationProvider가 매우 구체적인 인증을 여러 개 할 수 있는데 단지 1개의 AuthenticationManager 빈만 노출하면 됩니다.
ProviderManager는 AuthenticationProvider가 아무런 인증을 수행하지 못하는 경우에도 선택적 부모 AuthenticationManager를 구성할 수 있습니다. 부모는 모든 타입의 AuthenticaionManager도 될 수 있지만, 종종 ProviderManager 인스턴스입니다.
사실, ProvidrManager 인스턴스들은 같은 부모 AuthenticationManager를 공유할 수 있습니다. 공유된 AuthenticaionManager로부터 공통 인증을 가지고 있는 SecurityFilterChain 인스턴스들이 존재하는 경우에 일반적입니다.
기본적으로 ProviderManager는 성공적인 인증 요청으로 반환된 Authentication 객체의 민감한 credential 정보를 초기화합니다. 따라서 HttpSession에 필요 이상으로 긴 시간 동안 비밀번호 같은 정보가 남지 않도록 예방합니다.
하지만, 무상태 어플리케이션에서 성능을 향상하기 위해 사용자 객체의 캐시를 사용하는 경우 문제가 될 수 있습니다. 만약에 Authentication이 UserDetails 같은 객체 참조 정보를 캐시에 담아두었는데 ProviderManager가 credentials를 삭제하는 경우 캐시 된 값에서 더 이상 인증을 활용할 수 없습니다. 따라서 캐시를 사용하면 계정에 해당 정보를 넣어야 합니다. 확실한 해결 방법은 첫째로 캐시 구현체나 반환된 Authentication 객체를 생성하는 AuthenticationProvider에 객체의 복사본을 만드는 것입니다. 다음으로는, ProviderManager에서 eraseCredentialsAfterAuthentication 설정을 비활성화할 수 있습니다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
private List<AuthenticationProvider> providers; // (1)
...
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
Class<? extends Authentication> toTest = authentication.getClass();
Iterator var8 = this.getProviders().iterator(); // (2)
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) { // (3)
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication); // (4)
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (AuthenticationException var14) {
lastException = var14;
}
}
}
...
}
(1) AuthenticationProvider는 실제 인증을 수행하는데, 인터페이스므로 List에 의존하도록 하여 느슨한 관계를 만들 수 있습니다. 따라서 구현체가 변경되거나 구현체를 새로 넣거나 빼든 해당 부분은 변경되지 않습니다.
(2) (3) iterator()로 조회한 모든 AuthencationProvider들을 while문으로 검사하여 support 되는 경우에 인증을 수행합니다.
(4) 해당 AuthenticationProvider가 authentication()을 수행하며 결과에 따라서 부모가 인증을 수행하거나, credential 정보를 지우거나, ProviderNotFoundException 발생 등이 가능합니다.
- ProviderManager
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher;
private List<AuthenticationProvider> providers;
protected MessageSourceAccessor messages;
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication;
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, (AuthenticationManager)null);
}
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
this.eventPublisher = new NullEventPublisher();
this.providers = Collections.emptyList();
this.messages = SpringSecurityMessageSource.getAccessor();
this.eraseCredentialsAfterAuthentication = true;
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
this.checkState();
}
public void afterPropertiesSet() {
this.checkState();
}
private void checkState() {
if (this.parent == null && this.providers.isEmpty()) {
throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required");
}
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
Iterator var8 = this.getProviders().iterator();
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (AuthenticationException var14) {
lastException = var14;
}
}
}
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {
} catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
private void prepareException(AuthenticationException ex, Authentication auth) {
this.eventPublisher.publishAuthenticationFailure(ex, auth);
}
private void copyDetails(Authentication source, Authentication dest) {
if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;
token.setDetails(source.getDetails());
}
}
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
this.eraseCredentialsAfterAuthentication = eraseSecretData;
}
public boolean isEraseCredentialsAfterAuthentication() {
return this.eraseCredentialsAfterAuthentication;
}
private static final class NullEventPublisher implements AuthenticationEventPublisher {
private NullEventPublisher() {
}
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
public void publishAuthenticationSuccess(Authentication authentication) {
}
}
}
AuthenticationProvider
여러 개의 AuthenticationProvider들은 ProviderManager에 삽입됩니다. 각각 AuthenticationProvider는 구체적인 타입의 인증을 수행합니다. 예를 들어, DaoAuthenticationProvider는 아이디/비밀번호 기반 인증을 수행하며 JwtAuthenticationProvider는 JWT 토큰을 사용한 인증을 수행합니다.
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
Request Credentials with AuthenticationEntryPoint
AuthenticationEntryPoint는 클라이언트로부터 credential을 요청하는 HTTP 응답을 보낼 때 사용합니다.
때때로 클라이언트는 자원을 요청하기 위해 아이디/비밀번호와 같은 credential을 포함합니다. 이런 경우, 클라이언트는 이미 credential을 가지고 있으므로 스프링 시큐리티는 credential을 요청하는 HTTP 응답을 보낼 필요가 없습니다.
뒤집어 말하면, 클라이언트는 접근 권한이 없는 자원에 인증되지 않은 요청을 보냅니다. 이런 경우, AuthenticationEntryPoint의 구현체는 사용자로부터 credential을 요청하는 데 사용됩니다. AuthenticationEntryPoint 구현체는 로그인 페이지로 리다이렉트 하거나 WWW-Authenticate 헤더 응답 등등을 수행합니다.
public interface AuthenticationEntryPoint {
void commence(HttpServletRequest var1, HttpServletResponse var2, AuthenticationException var3)
throws IOException, ServletException;
}
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter는 사용자의 credential을 인증하기 위한 기본 Filter로 사용됩니다. credential들이 인증되기 이전에, 스프링 시큐리티는 AuthenticationEntryPoint를 사용하여 credential을 요청합니다.
그다음에, AbstractAuthenticationProcessingFilter는 어떠한 인증도 수행할 수 있습니다.
(1) 사용자가 credential을 제출하면, AbstractAuthenticationProcessingFilter는 인증된 HttpServletRequest로부터 Authentication을 생성합니다. 생성된 Authentication은 AbstractAuthenticationProcessingFilter 하위 클래스를 의존합니다. 예를 들어, UsernamePasswordAuthenticationFilter는 HttpServletRequest에 제출된 사용자 이름과 비밀번호로 UsernamePasswordAuthenticationToken을 생성합니다.
(2) Authentication은 인증된 AuthenticationManager로 통과합니다.
만약 인증이 실패하면
- SecurityContextHolder는 초기화됩니다.
- RememberMeService.loginFail이 호출됩니다. 만약 리멤버미가 설정되지 않았으면 아무런 작업이 없습니다.
- AuthenticationFailureHandler가 호출됩니다.
만약 인증이 성공하면
- SessionAuthenticationStrategy에 새로운 로그인을 알립니다.
- Authentication이 SecurityContextHolder에 설정됩니다. 나중에 SecurityContextPersistenceFilter는 HttpSession에 SecurityContext을 저장합니다.
- RememberMeServices.loginSuccess이 호출됩니다. 만약 리멤버미가 설정되지 않았으면 아무런 작업이 없습니다.
- ApplicationEventPublisher는 InteractiveAuthenticationSuccessEvent를 발행합니다.
- AuthenticationSuccessHandler가 호출됩니다.
- AbstractAuthenticationProcessingFilter
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
protected ApplicationEventPublisher eventPublisher;
protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private RequestMatcher requiresAuthenticationRequestMatcher;
private boolean continueChainBeforeSuccessfulAuthentication = false;
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
private boolean allowSessionCreation = true;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
this.setFilterProcessesUrl(defaultFilterProcessesUrl);
}
protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
public void afterPropertiesSet() {
Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (!this.requiresAuthentication(request, response)) {
chain.doFilter(request, response);
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = this.attemptAuthentication(request, response);
if (authResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authResult, request, response);
} catch (InternalAuthenticationServiceException var8) {
this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
this.unsuccessfulAuthentication(request, response, var8);
return;
} catch (AuthenticationException var9) {
this.unsuccessfulAuthentication(request, response, var9);
return;
}
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
this.successfulAuthentication(request, response, chain, authResult);
}
}
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return this.requiresAuthenticationRequestMatcher.matches(request);
}
public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication request failed: " + failed.toString(), failed);
this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
}
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
protected AuthenticationManager getAuthenticationManager() {
return this.authenticationManager;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setFilterProcessesUrl(String filterProcessesUrl) {
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(filterProcessesUrl));
}
public final void setRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requestMatcher;
}
public RememberMeServices getRememberMeServices() {
return this.rememberMeServices;
}
public void setRememberMeServices(RememberMeServices rememberMeServices) {
Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
this.rememberMeServices = rememberMeServices;
}
public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
}
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
protected boolean getAllowSessionCreation() {
return this.allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
}
public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
protected AuthenticationSuccessHandler getSuccessHandler() {
return this.successHandler;
}
protected AuthenticationFailureHandler getFailureHandler() {
return this.failureHandler;
}
}
'Spring > Spring Security' 카테고리의 다른 글
Spring Security 구조 메커니즘 (0) | 2024.05.17 |
---|---|
Security AccessDecisionManager 구현 학습하기 (0) | 2024.05.15 |
AuthToken 사용하기 (0) | 2021.10.26 |
JWT 토큰 (0) | 2021.10.26 |
임시권한 부여하기 (0) | 2021.10.23 |