Spring Security 原始碼淺析
1.核心元件
1.1.SecurityContextHolder
SecurityContextHolder用於儲存安全上下文(security context)的資訊。當前操作的使用者是誰,該使用者是否已經被認證,他擁有哪些角色許可權…這些都被儲存在SecurityContextHolder中。SecurityContextHolder預設使用ThreadLocal 策略來儲存認證資訊。看到ThreadLocal 也就意味著,這是一種與執行緒繫結的策略。Spring Security在使用者登入時自動繫結認證資訊到當前執行緒,在使用者退出時,自動清除當前執行緒的認證資訊。
1.2.Authentication
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
bject getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
- Authentication是spring security包中的介面,直接繼承自Principal類,而Principal是位於java.security包中的。可以見得,Authentication在spring security中是最高級別的身份/認證的抽象。
- 由這個頂級介面,我們可以得到使用者擁有的許可權資訊列表,密碼,使用者細節資訊,使用者身份資訊,認證資訊。
- getAuthorities(),許可權資訊列表,預設是GrantedAuthority介面的一些實現類,通常是代表權限資訊的一系列字串。
- getCredentials(),密碼資訊,使用者輸入的密碼字串,在認證過後通常會被移除,用於保障安全
- getDetails(),細節資訊,web應用中的實現介面通常為 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
- getPrincipal(),最重要的身份資訊,大部分情況下返回的是UserDetails介面的實現類,也是框架中的常用介面之一。UserDetails介面將會在下面的小節重點介紹。
1.3.AuthenticationManager
AuthenticationManager(介面)是認證相關的核心介面,也是發起認證的出發點,因為在實際需求中,我們可能會允許使用者使用使用者名稱+密碼登入,同時允許使用者使用郵箱+密碼,手機號碼+密碼登入,甚至,可能允許使用者使用指紋登入(還有這樣的操作?沒想到吧),所以說AuthenticationManager一般不直接認證,AuthenticationManager介面的常用實現類ProviderManager 內部會維護一個List列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// 維護一個AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
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 (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication資訊,則直接返回
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
//移除密碼
((CredentialsContainer) result).eraseCredentials();
}
//釋出登入成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
...
//執行到此,說明沒有認證成功,包裝異常資訊
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
}
1.4.AuthenticationProvider
AuthenticationProvider 就是實際的認證介面。比如DaoAuthenticationProvider,使用者提交的使用者名稱和密碼,被封裝成了UsernamePasswordAuthenticationToken,而根據使用者名稱載入使用者的任務則是交給了UserDetailsService ,DaoAuthenticationProvider中,對應的方法便是retrieveUser,雖然有兩個引數,但是retrieveUser只有第一個引數起主要作用,返回一個UserDetails。還需要完成UsernamePasswordAuthenticationToken和UserDetails密碼的比對,這便是交給additionalAuthenticationChecks方法完成的,如果這個void方法沒有拋異常,則認為比對成功。比對密碼的過程,用到了PasswordEncoder和SaltSource。
1.5.UserDetails
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
它和Authentication介面很類似,比如它們都擁有username,authorities,區分他們也是本文的重點內容之一。Authentication的getCredentials()與UserDetails中的getPassword()需要被區分對待,前者是使用者提交的密碼憑證,後者是使用者正確的密碼,認證器其實就是對這兩者的比對。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形成的。還記得Authentication介面中的getUserDetails()方法嗎?其中的UserDetails使用者詳細資訊便是經過了AuthenticationProvider之後被填充的。
1.6.UserDetailsService
UserDetailsService只負責從特定的地方(通常是資料庫)載入使用者資訊,僅此而已
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
1.7.AccessDecisionManager
AccessDecisionManager會呼叫AccessDecisionVoter進行投票,並根據結果進行決策。
- AffirmativeBased 一票通過,只要有一個投票器通過就允許訪問
- ConsensusBased 有一半以上投票器通過才允許訪問資源
- UnanimousBased 所有投票器都通過才允許訪問
1.8.AccessDecisionVoter
public interface AccessDecisionVoter<S> {
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
2.請求過程
主要的過濾器有:
- LogoutFilter
- BasicAuthenticationFilter Basic認證
- UsernamePasswordAuthenticationFilter 使用者認證,會呼叫AuthenticationManager進行認證
- SmsCodeAuthenticationFilter 簡訊認證
- OAuth2AuthenticationProcessingFilter OAuth2認證
- AnonymousAuthenticationFilter
- SessionManagementFilter
- FilterSecurityInterceptor 會呼叫AccessDecisionManager進行授權,授權通過會呼叫SecurityContextHolder.setContext(token.getSecurityContext())
3.boot方式的初始化過程
3.1時序圖
3.2類圖
AbstractConfiguredSecurityBuilder的doBuild方法:
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING;
beforeInit();
init();
buildState = BuildState.CONFIGURING;
beforeConfigure();
configure();
buildState = BuildState.BUILDING;
O result = performBuild();
buildState = BuildState.BUILT;
return result;
}
}