SpringSecurity-基本概念和處理流程

SpringSecurity-基本概念和處理流程分析

優秀文章推薦:
1、https://www.cnkirito.moe/spring-security-1/
2、https://www.jianshu.com/p/e8e0e366184e
文章較長,望耐心閱讀。

首先,需要明白SpringSecurity它解決什麼問題?
1、認證:證明你是你的問題,通常就是用用戶名和密碼來證明。
2、授權:你能幹什麼的問題,就像類似VIP和VVIP的區別。

1、基本類架構

圖片參考優秀文章中
圖片來自文章:https://www.cnkirito.moe/spring-security-1/

以上當作一個普通的類圖就行了,是一個經典的委託模式,後面給大家娓娓道來。

2、SpringSecurity是如何認證的呢?

無非就是比較你輸入的數據和服務器保存的數據是否一致嘛!
對,就是這樣的

1) 你輸入的用戶名和密碼,再SpringSecurity中的就映射爲Authentication
2)服務器記錄你的用戶名和密碼,在SpringSecurity中就映射爲UserDetails;
3) 那麼比較Authentication對象和UserDetails的相關內容,就可以解決你是不是你的問題了。

2.1 Authentication是一個接口

這個接口,可以得到用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息。
源碼中該接口描述:後面會講一下這是什麼意思,不慌!
Represents the token for an authentication request or for an authenticated principal once the request has been processed by the
{@link AuthenticationManager#authenticate(Authentication)} method.

package org.springframework.security.core;
/**
Represents the token for an authentication request or for an authenticated principal 
 once the request has been processed by the
  {@link AuthenticationManager#authenticate(Authentication)} method.
*/
public interface Authentication extends Principal, Serializable {
	//權限信息列表,默認是 GrantedAuthority 接口的一些實現類,通常是代表權限信息的一系列字符串。
	Collection<? extends GrantedAuthority> getAuthorities();
	//密碼信息,用戶輸入的密碼字符串,在認證過後通常會被移除,用於保障安全。
	Object getCredentials();
	//細節信息,web 應用中的實現接口通常爲 WebAuthenticationDetails,它記錄了訪問者的 ip 地址和 sessionId 的值。
	Object getDetails();
	//最重要的身份信息,大部分情況下返回的是 UserDetails 接口的實現類,也是框架中的常用接口之一。
	Object getPrincipal();
	boolean isAuthenticated();
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Object getPrincipal();這個方法會返回我們的UserDetails
Authentication是一個接口,Spring給我們提供了很多實現類,可以直接使用

  1. UsernamePasswordAuthenticationToken
  2. RememberMeAuthenticationToken
  3. AnonymousAuthenticationToken
  4. 。。。

2.2 UserDetails也是一個接口

這個接口主要由用戶實現,封裝了服務器中保存的用戶詳細信息,並且最後Authentication中的部分信息也由該對象提供
我自己可能說的不太清楚,可以上一段源碼描述:
Implementations are not used directly by Spring Security for security purposes. They simply store user information which is later encapsulated into {@link Authentication} objects. This allows non-security related user information (such as email addresses, telephone numbers etc) to be stored in a convenient location.

public interface UserDetails extends Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	String getPassword();
	String getUsername();
	boolean isAccountNonExpired();
	boolean isAccountNonLocked();
	boolean isCredentialsNonExpired();
	boolean isEnabled();

它和 Authentication 接口很類似,看見命名就知道意思了,說白了就是服務器中用戶信息的實體對象。

3、UserDetails從哪裏獲取?

瞭解到UserDetails是封裝了用戶信息的實體,那麼,我們的用戶信息從哪裏來呢?這是一個問題,自然會想到寫一個UserDetailsService從不同的數據源中獲取用戶數據了。
聰明,就是這樣
Spring這麼NB,當然給我們提供對應的接口,纔像話哇!

/**加載由於用戶信息的核心接口
Core interface which loads user-specific data.
*/
public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

就是這麼簡單,通過用戶名從數據源去查數據,至於怎麼查,到哪裏查,這就要自己操刀了哇!注意在這裏將用戶信息封裝成 UserDetails喲!

4、小夥子等等,我們的Authentication從哪裏找的哇?

  1. 前面,我們提到Authentication是封裝的用戶提交的用戶名和密碼或其它類型的認證信息;
  2. 這些信息使用提交HTTP請求提交到我們的web服務器的;
  3. SpringSecurity就是通過過濾和攔截這些請求獲得的request對象,從request對象中取出對應的請求信息,封裝成Authentication對象的;

Spring Security 是通過過濾器(Filter)和攔截器(Interceptor)實現應用安全控制。Spring Security 中定義和使用了很多的過濾器和攔截器,最重要的莫過於:AbstractAuthenticationProcessingFilter。
AbstractAuthenticationProcessingFilter 用於攔截認證請求,Spring Security API 文檔 中對 AbstractAuthenticationProcessingFilter 類的描述就可以發現,它是基於瀏覽器和 HTTP 認證請求的處理器,可以理解爲它就是 Spring Security 認證流程的入口。

AbstractAuthenticationProcessingFilter講解
過濾是比較基礎的概念了其中最重要的是:
doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 方法

由於類比較大,提取幾個關鍵位置看看。
抽象類:
	public abstract class AbstractAuthenticationProcessingFilter
字段:
	private AuthenticationManager authenticationManager;//後文詳解該對象
過濾器方法:
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }
            //這就是我們需要權限對象
            Authentication authResult;
            try {
             	//調用該方法操作
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }
                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                 //處理失敗時的操作
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }
            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            //處理成功時的操作
            this.successfulAuthentication(request, response, chain, authResult);
        }
    }

這句代碼很重要哦!後面會再次提到這個方法滴!
Authentication authResult = this.attemptAuthentication(request, response);
這就是我們獲取Authentication 對象的核心,該方法是一個抽象方法,當然從描述中我們知道這是一個抽象的過濾器,需要我們具體實現。在具體實現過程中們就可以根據需要構造自己的Authentication。

捋一下思路:
我們知道了如何將用戶提交的信息映射(封裝)成Authentication,涉及到核心接口和抽象類
    核心接口:Authentication
    核心抽象類:AbstractAuthenticationProcessingFilter
我們知道了如何獲得服務器端的用戶信息   UserDetails
	核心接口:UserDetails
	核心接口:UserDetailsService
這些接口是我們在開發中要進行實現內容。當然,我們可以仿造SpringSecurity提供的一些實現了來實現這些接口,這裏提供一些參考。
	 核心接口:Authentication,實現類:UsernamePasswordAuthenticationToken
	 核心抽象類:AbstractAuthenticationProcessingFilter,實現類:UsernamePasswordAuthenticationFilter
	 核心接口:UserDetails,實現類:User
	核心接口:UserDetailsService,實現類:CachingUserDetailsService

	思考一下:我們現在要做的是什麼?

對頭,比較Authentication和UserDetails中的信息

5、抓腦闊哇,這個怎麼比較呢?

在AbstractAuthenticationProcessingFilter代碼中:
提到了字段:
private AuthenticationManager authenticationManager;
提到了方法:
Authentication authResult = this.attemptAuthentication(request, response);
我們參考一下核心抽象類:AbstractAuthenticationProcessingFilter,實現類:UsernamePasswordAuthenticationFilter中的attemptAuthentication(request, response);方法的實現。

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
        //構造Authentication對象的參數,
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }
            if (password == null) {
                password = "";
            }
            username = username.trim();
            //Authentication 的實現類 UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            //這裏就是將Authentication和UserDetails比較的關鍵
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

這裏就是將Authentication和UserDetails比較的關鍵

== return this.getAuthenticationManager().authenticate(authRequest);==
也就是說,在獲得Authentication的過程中會將其與我們服務端的UserDetails進行比較。

6、來,關注一下比較(認證)的過程唄!

AuthenticationManager是個什麼鬼?

/**
描述:處理Authentication的接口
源碼描述:Processes an {@link Authentication} request.
*/
public interface AuthenticationManager {
	/**
	描述:你給我一個不算完整的Authentication,我給你一個完美的Authentication
	Attempts to authenticate the passed {@link Authentication} object, returning a
	fully populated <code>Authentication</code> object (including granted authorities)
	if successful.
	*/
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

ps(假裝看不到):我去,這樣的接口給我來十個,絲毫不慫!

AuthenticationManager 一般不需要我們實現,SpringSecurity依據提供了比較好的實現了。
我們來看一看這個接口其中的一個實現類(ProviderManager),以瞭解這個接口在幹嘛?

eee,類不算小,我還是挑重點的講哈子哇
類:
	public class ProviderManager implements AuthenticationManager
字段:
	private List<AuthenticationProvider> providers = Collections.emptyList();
核心方法:
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		。。。
		//對所有的AuthenticationProvider 遍歷,進行意義比較(認證)
		for (AuthenticationProvider provider : getProviders()) {
			。。
			try {
			//認證處理
				result = provider.authenticate(authentication);
				//// 如果有 Authentication 信息,則直接返回
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
		}
		。。。
		//如果有 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;
	}

我們發現這哥們AuthenticationManager的實現類不幹實事,就指揮一下子哇!然後將工作交給小弟,List
providers去處理了。

小夥計,不要暈哇。稍微梳理一下。
AuthenticationManager(接口)是認證相關的核心接口,也是發起認證的出發點,因爲在實際需求中,我們可能會允許用戶使用用戶名 + 密碼登錄,同時允許用戶使用郵箱 + 密碼,手機號碼 + 密碼登錄,甚至,可能允許用戶使用指紋登錄(還有這樣的操作?沒想到吧),所以說 AuthenticationManager 一般不直接認證;AuthenticationManager 接口的常用實現類 ProviderManager 內部會維護一個
List 列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。
也就是說,核心的認證入口始終只有一個:AuthenticationManager,不同的認證方式:用戶名 + 密碼(UsernamePasswordAuthenticationToken),郵箱 + 密碼,手機號碼 + 密碼登錄則對應了三個 AuthenticationProvider。
不同的認證方式,你就交給不同的小弟(AuthenticationProvider)處理處理就完了,我(AuthenticationManager)就調度一下就行了。

結論:比較交給了AuthenticationProvider,所以我們要如何進行比較,只需要定義自己的AuthenticationProvider就好了。

7、看一下AuthenticationProvider

比較過程的實際實現者,我們說AuthenticationManager是委託者,而AuthenticationProvider是被委託者,這就是啥委託模式吧!

/**
Indicates a class can process a specific
{@link org.springframework.security.core.Authentication} implementation.
*/
public interface AuthenticationProvider {
	//比較(認證)邏輯
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
	//是否開啓比較(認證)邏輯
	boolean supports(Class<?> authentication);
}

這裏,我們只需要將之前的UserDetailsService的實現類注入到AuthenticationProvider的實現類中,就可以讓Authentication和UserDetails進行比較了。
最後比較後的結果Authentication會依次返回
1 AuthenticationProvider 將結果返回給 AuthenticationManager
2 AbuthenticationManager 將結果返回給AbstractAuthenticationProcessingFilter
3 AbstractAuthenticationProcessingFilter 會將結果保存在SecurityContextHolder中

SecurityContextHolder.getContext().setAuthentication(authResult);

7.1 SecurityContextHolder

SecurityContextHolder 用於存儲安全上下文(security context)的信息。當前操作的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權限… 這些都被保存在 SecurityContextHolder 中。SecurityContextHolder 默認使用 ThreadLocal 策略來存儲認證信息。看到 ThreadLocal 也就意味着,這是一種與線程綁定的策略。Spring Security 在用戶登錄時自動綁定認證信息到當前線程,在用戶退出時,自動清除當前線程的認證信息。但這一切的前提,是你在 web 場景下使用 Spring Security,而如果是 Swing 界面,Spring 也提供了支持,SecurityContextHolder 的策略則需要被替換。

//獲得認證對象Authentication
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
//設置認證對象Authentication
SecurityContextHolder.getContext().setAuthentication(authResult);

整個認證過程如下時序圖參考
在這裏插入圖片描述

總結

SpringSecurity是一個很複雜的系統,在此筆者僅僅是對其基本思想和核心概念,按照自己的心中的疑惑,以及解決以後的過程做了一個膚淺的記錄。
如有不足之處,望留言評論,本文參考了文章前提到的幾篇我認爲很優秀的文章,作者寫的很好。

最後,希望讀者可以通過本文理解使用SpringSecurity需要關心的幾個問題
1、用戶輸入的數據以Authentication的形式進入SpringSecurity
2、用戶在服務器保存的信息以UserDetails的形式 進入SpringSecurity
3、通過UserDetailsService可以獲得UserDetails
4、通過AbstractAuthenticationProcessingFilter的實現了可以獲得Authentication
5、在AbstractAuthenticationProcessingFilter通過AuthenticationManager完成比較(認證)操作
6、SpringSecurity 採用委託模式將比較(認證)任務委託給AuthenticationProvider 進行實際處理
7、認證通過的Authentication信息保存在默認使用 ThreadLocal 策略的SecurityContextHolder中

望回覆文章前的類圖,或許會有不一樣的收穫!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章