淺析 Spring Security 核心組件
前言
近幾天在網上找了一個 Spring Security 和 JWT 的例子來學習,項目地址是
https://github.com/szerhusenBC/jwt-spring-security-demo
還是有發現的,作為該研究 的一篇文章,通過對Spring SecuritySpring Security 的一個優秀的流程進行分析,并沒有參考這篇文章先對 Spring Security 的流程進行分析,并想寫一篇官方文檔和 Spring Security 的核心組件一些大佬寫的Spring Security分析文章,有雷同的地方還請見諒。
Spring Security的核心類
Spring Security 的核心類主要包括以下幾個:
- SecurityContextHolder :存放身份信息的容器
- Authentication : 身份信息的抽象接口
- AuthenticationManager : 身份認證器,認證的核心接口
- UserDetailsS??ervice:一般用于從數據庫中加載身份信息
- UserDetails : 比較認證,有更詳細的身份信息
SecurityContextHolder、Securityontext和Authentication
SecurityContextHolder存儲安全信息(安全上下文)的信息,即用于存儲身份,認證信息等的包含。SecurityContextHolder默認使用 ThreadLocal策略來存儲認證信息,即一種與線程綁定的策略,各個線程執行時都可以獲取該線程中的安全時間(安全上下文),影響線程中的線程如果在請求中的安全時間互不影響。
因為身份信息是與當前會話的關系,所以我們可以在程序的任何地方使用獲取用戶信息的方法,登錄用戶的姓名的例子如下:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
getAuthentication()返回方法獲得了認證信息,準確的說是一個 Authentication實例,AuthenticationSpring Security 中的一個重要接口,直接繼承自 Principal 類,應該表示對用戶身份信息的抽象,接口來源如下:
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、ProviderManager 和 AuthenticationProvider
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所有屬性都會被填充。
Spring Security 中,一個自己AuthenticationManager默認 對進行請求ProviderManager,ProviderManager將派給給自己 AuthenticationProvider的每個人都需要在一個目標列表中 實現目標的AuthenticationProvider跟蹤服務提供者。驗證結果只有兩種情況:拋出一個異常或者完全填充一個 對象的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成功成功失敗成功嘗試,認證即返回失敗,如果所有的Provider都認證,認證失敗返回 ProviderManager無效 ProviderNotFoundException。
是一個接口,接口定義如下:AuthenticationProvider
public interface AuthenticationProvider {
//認證方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//該Provider是否支持對應的Authentication
boolean supports(Class<?> authentication);
}
在 ProviderManager的 Javadoc 曾提到,
如果多個 AuthenticationProvider 支持傳遞的 Authentication 對象,則第一個能夠成功驗證 Authentication 對象的人會確定結果,并覆蓋之前支持的 AuthenticationProvider 拋出的任何可能的 AuthenticationException 。成功認證后,不會嘗試后續的 AuthenticationProvider 。如果任何支持 AuthenticationProvider 的身份驗證未成功,則最后拋出的 AuthenticationException 將被重新拋出
大致意思是:
如果有多個AuthenticationProvider都支持同一個Authentication對象,那么第一個能夠成功驗證的Provder將填充 其返回結果,那么早期支持的AuthenticationProvider拋出可能的AuthenticationException。成功驗證后,將如果所有驗證都不會在中驗證)的AuthenticationProvider。如果 AuthenticationProvider沒有成功驗證,則將拋出最后的ProviderProvider拋出異常。(AuthenticationProvider可以配置配置類的配置)PS:
不同的AuthenticationProvider認證支持不同的不同的對象,分別對應不同的Authentication對象,那么當一個 不同的對象AuthenticationProvier進入ProviderManager的內部時, 它們會在AuthenticationProvider它們中挑選其對的提供者相應的對象進行驗證。
不同的認證邏輯是不同的,即 使用用戶名和密碼,使用用戶名和密碼,登錄提供了簡單的實現 ,這也是 一個用戶在登錄時使用的方式AuthenticationProvider,如果用戶名和密碼不一樣,那么 它也是一個用戶可以登錄的方式。密碼和 我們一般要在接口中,并把 Spring Security 配置類實現其配置,這樣也用于在使用中進行詳細的認證,然后該接口返回一個,它包含了身份信息,比如從數據庫拿取的信息密碼和AuthenticationProvider的認證核心,即加載的 來列表用戶的密碼是否匹配,用戶詳情和驗證以及其他的密碼(關于 和就是使用的介紹在下面介紹。)。,比如QQ登錄,那么就需要設置的 ,這里就不細說了。AuthenticationProviderDaoAuthenticationProviderUserDetailsServiceGrantedAuthorityUserDetailsServiceDaoAuthenticationProviderUserDetailsUserDetailsUserDetailsServiceUserDetailsAuthenticationProvider
認證成功后清除驗證信息
在 ProviderManager 的源中我還發現一點,在認證成功后清除驗證信息,如下:
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
//成功認證后刪除驗證信息
((CredentialsContainer) result).eraseCredentials();
}
從安全 3.1,在請求認證成功后 ProviderManager刪除 Authenticationspring 中的認證信息,準確地說,一般刪除是密碼信息,這保證跟密碼的安全。我可以直接執行刪除操作的步驟如下:
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();
}
}
}
從就可以很舒服地使用私人密碼。
UserDetailsS??ervice 和 UserDetails
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 接口類似,它們有用戶名、權限。它們的區別如下:
- Authentication的getCredentials()一般與Users中的getPassword()不一樣,是用戶提交的密碼,后面是用戶正確的從數據庫加載的密碼),
AuthenticationProvider分別對中進行對比。 - Authentication 中的 getAuthorities() 形成是由 UserDetails 的 getAuthorities() 傳遞的。
- Authentication 中的 getUserDetails() 中的 UserDetails 用戶詳細信息是經過
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 核心才能解釋 Spring Security 的一些詳細的分析認證過程。