淺析Spring Security 核心元件

pjmike_pj發表於2018-10-12

原文部落格地址: pjmike的部落格

前言

近幾天在網上找了一個 Spring Security 和JWT 的例子來學習,專案地址是: github.com/szerhusenBC… 作為學習Spring Security還是不錯的,通過研究該 demo 發現自己對 Spring Security一知半解,並沒有弄清楚Spring Seurity的流程,所以才想寫一篇文章先來分析分析Spring Security的核心元件,其中參考了官方文件及其一些大佬寫的Spring Security分析文章,有雷同的地方還請見諒。

Spring Security的核心類

Spring Security的核心類主要包括以下幾個:

  • SecurityContextHolder: 存放身份資訊的容器
  • Authentication: 身份資訊的抽象介面
  • AuthenticationManager: 身份認證器,認證的核心介面
  • UserDetailsService: 一般用於從資料庫中載入身份資訊
  • UserDetails: 相比Authentication,有更詳細的身份資訊

SecurityContextHolder、Securityontext和Authentication

SecurityContextHolder用於儲存安全上下文(security context)的資訊,即一個儲存身份資訊,認證資訊等的容器。SecurityContextHolder預設使用 ThreadLocal策略來儲存認證資訊,即一種與執行緒繫結的策略,每個執行緒執行時都可以獲取該執行緒中的 安全上下文(security context),各個執行緒中的安全上下文互不影響。而且如果說要在請求結束後清除安全上下文中的資訊,利用該策略Spring Security也可以輕鬆搞定。

因為身份資訊時與執行緒繫結的,所以我們可以在程式的任何地方使用靜態方法獲取使用者資訊,一個獲取當前登入使用者的姓名的例子如下:

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類,該介面表示對使用者身份資訊的抽象,介面原始碼如下:

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預設的實現類是 ProviderManagerProviderManager並不是自己直接對請求進行驗證,而是將其委派給一個 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異常。

事實上,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兩者的密碼(關於 UserDetailsServiceUserDetails的介紹在下面小節介紹。)。而如果是使用第三方登入,比如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

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。它們的區別如下:

  • Authentication 的 getCredentials() 與 UserDetails 中的 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 Security的認證過程。

參考資料 & 鳴謝

相關文章