모두 Spring Security 공식 문서를 참고해 정리했습니다.
Spring Security는 filter
와 annotation
을 사용해서 애플리케이션 보안을 손 쉽게 도와줍니다.
애플리케이션의 보안은 크게 두 개의 독립적인 문제로 요약됩니다.
Authentication
: who are you?, 누구인지?
Authorization
: what are you allowed to do?, 어느 권한을 가졌는지?
Spring Security는 이 둘을 분리해서 각자의 전략과 확장을 제공합니다.
먼저, Authentication과 Authorization을 분리해서 정리하겠습니다.
Authentication의 가장 중요한 전략으로는 interface AuthenticationManager
입니다. (구현체로는 ProviderManager
)
AuthenticationManager.java
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
authenticate()
이라는 인증을 진행하는 메서드 하나만을 가지고 있습니다.
여기서 AuthenticationProvider
가 등장합니다. (용어가 굉장히 헷갈리니 제대로 봐야합니다..ㅜ)
AuthenticationProvider.java
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
두 interface의 메서드를 확인해보면 return type, parameter가 같은 authenticate()
가 있습니다.
AuthenticationProvider
에는 supports()
라는 메서드가 있다는 게 둘의 차이점입니다. 이 메서드는 내가 이 객체를 authenticate()
할 수 있는가? 를 return합니다.
사실상 파라미터인 Class<?>
-> Class<? extends Authentication>
인 겁니다.
두 객체는 협업을 통해 Authentication을 진행합니다. 엄밀히 말하자면 AuthenticationManager
--delegate-->AuthenticationProvider
이다.
아래 그림이 가장 이해하기 좋은 것 같습니다.
두 객체는 AuthenticationManager
1 : N AuthenticationProvider
의 관계를 맺고 있습니다.
추가로 ProviderManager
소스 코드도 보여드리겠습니다. (매우 길어 정말 필요한 부분만 남겼습니다.) 보기 힘들지만 편의를 위해 달아놓은 !!!!! #
부분만 따라오시면 됩니다.
ProviderManager.java
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
!!!!!! 1
private List<AuthenticationProvider> providers = Collections.emptyList();
!!!!!! 2
private AuthenticationManager parent;
public ProviderManager(AuthenticationProvider... providers) {
this(Arrays.asList(providers), null);
}
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
int size = this.providers.size();
!!!!! 3
for (AuthenticationProvider provider : getProviders()) {
!!!!! 4
if (!provider.supports(toTest)) {
continue;
}
try {
!!!!! 5
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
!!!!! 6
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentcation);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
!!!!! 1
1:N의 관계로 AuthenticationProvider를 List로 가지고 있습니다.
!!!!! 2
밑에서 설명하겠습니다.
!!!!! 3
가지고 있는 provider들 중에
!!!!! 4
자신의 Authentication
객체를 지원하는 provider를 찾아
!!!!! 5
해당 provider가 authenticate()
을 수행하도록 합니다.
!!!!! 6
밑에서 설명
이렇게 두 객체는 협력하게 됩니다.
ProviderManager
의 !!!!! 2
를 보면 똑같은 객체를 parent
라는 이름으로 가지고 있습니다.
ProviderManager
는 자신의 provider들 중에서 support하는 provider로 authenticate()
을 진행합니다. 하지만 가진 provider가 모두 지원하지 못한다면 자신의 parent에게 authenticate를 의뢰합니다. - !!!!! 6
만약 인증 실패 / 부모가 없거나 / 부모도 지원하지 못한다면 -> AuthenticationException
예외를 발생하게 됩니다.
User의 Authentication을 진행할 때, 가장 중요한 전략으로 AuthenticaionManager
를 이용하게 된다.
AuthenticaionManager
는 자신에게 인증 요청이 들어온 Authentication
객체를 인증하게 된다. 이때 인증 행위를 위임한 AuthenticationProvider
들을 탐색해서 이 객체를 인증할 수 있는 provider가 있는지 찾게 된다.
찾게 됐다면 그 provider가 인증을 진행하도록 한다. 만약 찾지 못했다면, 자신의 parent Manager에게 의뢰하게 되고, parent는 위와 같이 똑같은 행위를 하게 된다.
결과적으로 인증에 성공했다면, 성공한 Authentication
객체를 반환하고, 실패한다면 AuthenticationException
을 던지게 된다.
인증이 성공적이면, 이제 authorization(인가)을 진행하게 된다. 인가의 주요한 전략은 AccessDecisionManager
이다.
이는 세 구현체를 갖는데
(소스코드를 찾아보니 AccessDecisionManager
를 구현한 AbstractAccess~~
의 구현 클래스들이다.)
이 AccessDecisionManager
는 인가를 AccessDecisionVoter
들에게 위임하게 된다.
구조가 Authenticaion과 비슷하다.
AuthManager -> AuthProviders delegate --- Authentication
AccessDeManager -> AccessDeVoters delegate --- Authorization
하지만 인가에서는 parent라는 개념은 존재하지 않습니다.
AccessDecisionVoter의 메서드를 보면
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
가 있는데
중간에 파라미터인 Object와 ConfigAttribute는
주로 User의 Role인 ROLE_ADMIN 등
인증이 성공적이라면, 인가를 진행하는데,
AccessDecisionManager
가 요청 받은 secured Object에 인가를 진행할 수 있는 AccessDecisionVoter
를 자신이 가진 voter들 중에 찾게 된다. voter는 User가 secured Object에 접근할 수 있는지 ConfigAttribute를 참고해 판단하게 된다.
어느 voter라도 접근을 허용한다면 ACCESS_GRANTED
, ACCESS_DENIED
혹은 ACCESS_ABSTAIN
도 존재한다.
Spring Security는 Servlet의 filter를 기반으로 만들어졌습니다.
Servlet의 filter를 간단하게 말하자면, 모든 request는 Servlet에 도착하기 전에 순서를 가진 filter들인
filter chian
를 타게 됩니다.
Spring Security는 이 filter chain에 하나의 filter를 추가함으로써 구현됩니다. 이 filter의 타입은 FilterChainProxy
입니다.
이 FilterChainProxy
는 Spring Container에서 Bean으로 관리되며, 기본적으로 생성돼 모든 request에 적용이 됩니다.
그리고 default 순서를 가집니다. SecurityProperties.DEFAULT_FILTER_ORDER
위에 볼드체로 말했듯, FilterChainProxy
는 Spring Container의 입장에서 하나의 filter에 불과합니다. 하지만, 그 내부에는 많은 filter들이 존재하고 각기 다른 기능을 수행합니다.
또 다시 많은 용어들이 등장하는데, 사실 FilterChainProxy
는 DelegatingFilterProxy
의 위임을 받는 bean입니다.
Security Filter에는 FilterChainProxy
를 가진 (Spring Container 내부에서는 springSecurityFilterChain
라는 이름을 가진 bean입니다.) DelegatingFilterProxy
가 등록되게 되고, filter chain 역할 위임을 받은 FilterChainProxy
가 내부의 filter들을 통해 동작하게 됩니다.
공식 문서에서 자꾸 양파처럼 "~~는 사실 ~~내부에 있다."라는 식으로 이야기한다.
SecurityFilterChain
이 등장하는데,
DelegatingFilterProxy
는 내부에FilterProxyChain
bean을 가지고 있고,FilterChainProxy
는 내부에 여러SecurityChainProxy
bean을 갖고 있다. 하나의SecurityChainProxy
에는 여러 Security Filter가 존재한다.
이렇게 하는 이유는 DelegatingFilterProxy
는 굳이 bean일 필요가 없다는 것과 연관있는 것 같습니다. FilterChainProxy
는 하나의 filter에 불과하다고 했는데, 서블릿 컨테이너에 하나의 filter로 등록하기 위해(반드시 bean일 필요X) DelegatingFilterProxy
가 존재하고, 내부에 FilterChainProxy
와 SecurityFilterChain
은 bean으로 관리하게 됩니다.
이 그림이 더 정확한 구조를 나타내는 그림인 것 같습니다.
FilterChainProxy
는 여러 SecurityFilterChain
을 가진다고 했는데, FilterChainProxy
가 어느 SecurityFilterChain
을 사용할 지는 매치 해놓은 URI를 통해 결정하게 된다.
FilterProxyChain에 등록된 filter와 순서로는
ForceEagerSessionCreationFilter
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter
이렇게 많습니다. 순서까지 전부 다 알고 있을 필요는 없다고 합니다.
https://docs.spring.io/spring-security/reference/servlet/architecture.html
https://docs.gigaspaces.com/security/introducing-spring-security.html