原文博客地址: pjmike的博客html
近幾天在網上找了一個 Spring Security 和JWT 的例子來學習,項目地址是: github.com/szerhusenBC… 做爲學習Spring Security仍是不錯的,經過研究該 demo 發現本身對 Spring Security
只知其一;不知其二,並無弄清楚Spring Seurity的流程,因此纔想寫一篇文章先來分析分析Spring Security的核心組件,其中參考了官方文檔及其一些大佬寫的Spring Security分析文章,有雷同的地方還請見諒。java
Spring Security的核心類主要包括如下幾個:git
SecurityContextHolder
用於存儲安全上下文(security context)的信息,即一個存儲身份信息,認證信息等的容器。SecurityContextHolder
默認使用 ThreadLocal
策略來存儲認證信息,即一種與線程綁定的策略,每一個線程執行時均可以獲取該線程中的 安全上下文(security context),各個線程中的安全上下文互不影響。並且若是說要在請求結束後清除安全上下文中的信息,利用該策略Spring Security也能夠輕鬆搞定。github
由於身份信息時與線程綁定的,因此咱們能夠在程序的任何地方使用靜態方法獲取用戶信息,一個獲取當前登陸用戶的姓名的例子以下:web
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
複製代碼
getAuthentication()
方法返回了認證信息,準確的說是一個 Authentication
實例,Authentication
是 Spring Security 中的一個重要接口,直接繼承自 Principal類,該接口表示對用戶身份信息的抽象,接口源碼以下:spring
public interface Authentication extends Principal, Serializable {
//權限信息列表,默認是 GrantedAuthority接口的一些實現
Collection<? extends GrantedAuthority> getAuthorities();
//密碼信息,用戶輸入的密碼字符串,認證後一般會被移除,用於保證安全
Object getCredentials();
//細節信息,web應用中一般的接口爲 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值
Object getDetails();
//身份信息,返回UserDetails的實現類
Object getPrincipal();
//認證狀態,默認爲false,認證成功後爲 true
boolean isAuthenticated();
//上述身份信息是否通過身份認證
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
複製代碼
AuthenticationManager是身份認證器,認證的核心接口,接口源碼以下:數據庫
public interface AuthenticationManager {
/** * Attempts to authenticate the passed {@link Authentication} object, returning a * fully populated <code>Authentication</code> object (including granted authorities) * @param authentication the authentication request object * * @return a fully authenticated object including credentials * * @throws AuthenticationException if authentication fails */
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
複製代碼
該接口只有一個 authenticate()
方法,用於身份信息的認證,若是認證成功,將會返回一個帶了完整信息的Authentication
,在以前提到的Authentication
全部的屬性都會被填充。api
在Spring Security中,AuthenticationManager
默認的實現類是 ProviderManager
,ProviderManager
並非本身直接對請求進行驗證,而是將其委派給一個 AuthenticationProvider
列表。列表中的每個 AuthenticationProvider
將會被依次查詢是否須要經過其進行驗證,每一個 provider的驗證結果只有兩個狀況:拋出一個異常或者徹底填充一個 Authentication
對象的全部屬性。ProviderManager
中的部分源碼以下:安全
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
//維護一個AuthenticationProvider 列表
private List<AuthenticationProvider> providers = Collections.emptyList();
private AuthenticationManager parent;
//構造器,初始化 AuthenticationProvider 列表
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();
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
// AuthenticationProvider 列表中每一個Provider依次進行認證
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
...
try {
//調用 AuthenticationProvider 的 authenticate()方法進行認證
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
// 若是 AuthenticationProvider 列表中的Provider都認證失敗,且以前有構造一個 AuthenticationManager 實現類,那麼利用AuthenticationManager 實現類 繼續認證
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
...
catch (AuthenticationException e) {
lastException = e;
}
}
//認證成功
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
//成功認證後刪除驗證信息
((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;
}
複製代碼
ProviderManager中的 authenticationManager
列表依次去嘗試認證,認證成功即返回,認證失敗返回null,若是全部的 Provider都認證失敗, ProviderManager
將會拋出一個 ProviderNotFoundException
異常。bash
事實上,AuthenticationProvider
是一個接口,接口定義以下:
public interface AuthenticationProvider {
//認證方法
Authentication authenticate(Authentication authentication) throws AuthenticationException;
//該Provider是否支持對應的Authentication
boolean supports(Class<?> authentication);
}
複製代碼
在 ProviderManager
的 Javadoc曾提到,
If more than one AuthenticationProvider supports the passed Authentication object, the first one able to successfully authenticate the Authentication object determines the result, overriding any possible AuthenticationException thrown by earlier supporting AuthenticationProvider s. On successful authentication, no subsequent AuthenticationProvider s will be tried. If authentication was not successful by any supporting AuthenticationProvider the last thrown AuthenticationException will be rethrown
大體意思是:
若是有多個 AuthenticationProvider 都支持同一個Authentication 對象,那麼第一個 可以成功驗證Authentication的 Provder 將填充其屬性並返回結果,從而覆蓋早期支持的 AuthenticationProvider拋出的任何可能的 AuthenticationException。一旦成功驗證後,將不會嘗試後續的 AuthenticationProvider。若是全部的
AuthenticationProvider
都沒有成功驗證 Authentication,那麼將拋出最後一個Provider拋出的AuthenticationException。(AuthenticationProvider能夠在Spring Security配置類中配置)
PS:
固然有時候咱們有多個不一樣的
AuthenticationProvider
,它們分別支持不一樣的Authentication
對象,那麼當一個具體的AuthenticationProvier
傳進入ProviderManager
的內部時,就會在AuthenticationProvider
列表中挑選其對應支持的provider對相應的 Authentication對象進行驗證。
不一樣的登陸方式認證邏輯是不同的,即 AuthenticationProvider
會不同,若是使用用戶名和密碼登陸,那麼在Spring Security 提供了一個 AuthenticationProvider
的簡單實現 DaoAuthenticationProvider
,這也是框架最先的 provider,它使用了一個 UserDetailsService
來查詢用戶名、密碼和 GrantedAuthority
,通常咱們要實現UserDetailsService
接口,,並在Spring Security配置類中將其配置進去,這樣也促使使用DaoAuthenticationProvider
進行認證,而後該接口返回一個UserDetails
,它包含了更加詳細的身份信息,好比從數據庫拿取的密碼和權限列表,AuthenticationProvider 的認證核心就是加載對應的 UserDetails
來檢查用戶輸入的密碼是否與其匹配,即UserDetails和Authentication二者的密碼(關於 UserDetailsService
和UserDetails
的介紹在下面小節介紹。)。而若是是使用第三方登陸,好比QQ登陸,那麼就須要設置對應的 AuthenticationProvider
,這裏就不細說了。
在上面ProviderManager的源碼中我還發現一點,在認證成功後清除驗證信息,以下:
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
//成功認證後刪除驗證信息
((CredentialsContainer) result).eraseCredentials();
}
複製代碼
從 spring Security 3.1以後,在請求認證成功後 ProviderManager
將會刪除 Authentication
中的認證信息,準確的說,通常刪除的是 密碼信息,這能夠保證密碼的安全。我跟了一下源碼,實際上執行刪除操做的步驟以下:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
public void eraseCredentials() {
super.eraseCredentials();
//使密碼爲null
this.credentials = null;
}
}
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
...
public void eraseCredentials() {
//擦除密碼
this.eraseSecret(this.getCredentials());
this.eraseSecret(this.getPrincipal());
this.eraseSecret(this.details);
}
private void eraseSecret(Object secret) {
if (secret instanceof CredentialsContainer) {
((CredentialsContainer)secret).eraseCredentials();
}
}
}
複製代碼
從源碼就能夠看出實際上就是擦除密碼操做。
UserDetailsService
簡單說就是加載對應的UserDetails
的接口(通常從數據庫),而UserDetails
包含了更詳細的用戶信息,定義以下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
複製代碼
UserDetails 接口與 Authentication接口類似,它們都有 username、authorities。它們的區別以下:
AuthenticationProvider
就會對二者進行對比。AuthenticationProvider
認證以後填充的。下面來看一個官方文檔提供的例子,代碼以下:
public class SpringSecuriryTestDemo {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(request);
break;
} catch (AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication());
}
static class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getName().equals(authentication.getCredentials())) {
return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
}
複製代碼
測試以下:
Please enter your username:
pjmike
Please enter your password:
123
Authentication failed: Bad Credentials
Please enter your username:
pjmike
Please enter your password:
pjmike
Successfully authenticated.
Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230:
Principal: pjmike;
Credentials: [PROTECTED];
Authenticated: true; Details: null;
Granted Authorities: ROLE_USER
複製代碼
上面的例子很簡單,不是源碼,只是爲了演示認證過程編寫的Demo,並且也缺乏過濾器鏈,可是麻雀雖小,五臟俱全,基本包括了Spring Security的核心組件,表達了Spring Security 認證的基本思想。解讀一下:
UsernamePasswordAuthentication
的實例中(該類是 Authentication
接口的實現)Authentication
傳遞給 AuthenticationManager
進行身份驗證AuthenticationManager
會返回一個徹底填充的 Authentication
實例,該實例包含權限信息,身份信息,細節信息,可是密碼一般會被移除SecurityContextHolder.getContext().setAuthentication(…)
傳入上面返回的填充了信息的 Authentication
對象經過上面一個簡單示例,咱們大體明白了Spring Security的基本思想,可是要真正理清楚Spring Security的認證流程這還不夠,咱們須要深刻源碼去探究,後續文章會更加詳細的分析Spring Security的認證過程。
這篇文章主要分析了Spring Security的一些核心組件,參考了官方文檔及其相關譯本,對核心組件有一個基本認識後,才便於後續更加詳細的分析Spring Security的認證過程。