Spring Security認證與授權的原理

Spring Security所解決的問題就是安全訪問控制,而安全訪問控制功能其實就是所有進入系統的請求進行攔截,校驗每一個請求是否能夠訪問它所期望的資源,可以通過Filter和AOP等技術來實現,Spring Security對web資源的保護是靠Filter來實現的。

當初始化Spring Security時,會創建一個名爲SpringSecurityFilterChain的servlet過濾器,類型是org.springframework.security.web.FilterChainProxy,它實現了javax.servlet.Filter,因此外部請求的類都會經過此類。如圖:

實際上就是通過了一個filter鏈進行過濾,實際進行認證的類是AuthenticationManager,授權的類是AccessDecisionManager。

(1)SecurityContextPersistenceFilter:這個Filter是整個攔截過程的入口和出口(也就是第一個和最後- 一個攔截器) , 會在請求開始時從配置好的SecurityContextRepository中獲取SecurityContext ,然後把它設置給SecurityContextHolder.在請求完成後將SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository ,同時清除securityContextHolder所持有的SecurityContext ; .

(2)UsernamePasswordAuthenticationFilter:用於處理來自表單提交的認證。該表單必須提供對應的用戶名和密碼,其內部還有登錄成功或失敗後進行處理的AuthenticationSuccessHandler和AuthenticationFailureHandler ,這些都可以根據需求做相關改變;

(3)FilteSecurityInterceptor:是用於保護web資源的,使用AccessDecisionManager對當前用戶進行授權訪問,前面已經詳細介紹過了;

(4)ExceptionTranslationFilter:ExceptionTranslationFilter能夠捕獲來自FilterChain 所有的異常,並進行處理。但是它只會處理兩類異常:ExceptionTranslationFilter能夠捕獲來自FilterChain 所有的異常,並進行處理。但是它只會處理兩類異常:
AuthenticationException和AccessDeniedException ,其它的異常它會繼續拋出。

認證流程:

(1)用戶提交用戶名、密碼被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 過濾器獲取到,
封裝爲請求Authentication,通常情況下是UsernamePasswordAuthenticationToken這個實現類。
(2)然後過濾器將Authentication提交至認證管理器(AuthenticationManager)進行認證
(3)認證成功後, AuthenticationManager 身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,
身份信息,細節信息,但密碼通常會被移除) Authentication 實例。
(4)SecurityContextHolder 安全上下文容器將第3步填充了信息的 Authentication ,通過 SecurityContextHolder.getContext().setAuthentication(…)方法,設置到其中。 可以看出AuthenticationManager接口(認證管理器)是認證相關的核心接口,也是發起認證的出發點,它 的實現類爲ProviderManager。而Spring Security支持多種認證方式,因此ProviderManager維護着一個 List<AuthenticationProvider> 列表,存放多種認證方式,最終實際的認證工作是由 AuthenticationProvider完成的。咱們知道web表單的對應的AuthenticationProvider實現類爲 DaoAuthenticationProvider,它的內部又維護着一個UserDetailsService負責UserDetails的獲取。最終 AuthenticationProvider將UserDetails填充至Authentication

 

這裏面比較重要的就是第一個filter和provider。他們會去查我們的數據庫或者定義在內存中的UserDataService。大家可以看一下這幾類的代碼,debug看看。

AuthenticationProvider

通過前面的Spring Security認證流程我們得知,認證管理器(AuthenticationManager)委託
AuthenticationProvider完成認證工作。
AuthenticationProvider是一個接口,定義如下:
public interface AuthenticationProvider { 
    Authentication authenticate(Authentication authentication) throws    AuthenticationException; 
    boolean supports(Class<?> var1); 
}
authenticate()方法定義了認證的實現過程,它的參數是一個Authentication,裏面包含了登錄用戶所提交的用
戶、密碼等。而返回值也是一個Authentication,這個Authentication則是在認證成功後,將用戶的權限及其他信
息重新組裝後生成。
Spring Security中維護着一個 List<AuthenticationProvider> 列表,存放多種認證方式,不同的認證方式使用不
同的AuthenticationProvider。如使用用戶名密碼登錄時,使用AuthenticationProvider1,短信登錄時使用
AuthenticationProvider2等等這樣的例子很多。
每個AuthenticationProvider需要實現supports()方法來表明自己支持的認證方式,如我們使用表單方式認證,
在提交請求時Spring Security會生成UsernamePasswordAuthenticationToken,它是一個Authentication,裏面
封裝着用戶提交的用戶名、密碼信息。而對應的,哪個AuthenticationProvider來處理它?
我們在DaoAuthenticationProvider的基類AbstractUserDetailsAuthenticationProvider發現以下代碼:
public boolean supports(Class<?> authentication) { 
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); 
}
也就是說當web表單提交用戶名密碼時,Spring SecurityDaoAuthenticationProvider處理。
最後,我們來看一下Authentication(認證信息)的結構,它是一個接口,我們之前提到的
UsernamePasswordAuthenticationToken就是它的實現之一:
public interface Authentication extends Principal, Serializable { (1) 
    Collection<? extends GrantedAuthority> getAuthorities(); (2) 
    Object getCredentials(); (3) 
    Object getDetails(); (4)
    Object getPrincipal(); (5) 
    boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException;
 }
1Authenticationspring security包中的接口,直接繼承自Principal類,而Principal是位於 java.security
包中的。它是表示着一個抽象主體身份,任何主體都有一個名稱,因此包含一個getName()方法。
2getAuthorities(),權限信息列表,默認是GrantedAuthority接口的一些實現類,通常是代表權限信息的一系
列字符串。
3getCredentials(),憑證信息,用戶輸入的密碼字符串,在認證過後通常會被移除,用於保障安全。
4getDetails(),細節信息,web應用中的實現接口通常爲 WebAuthenticationDetails,它記錄了訪問者的ip
址和sessionId的值。
5getPrincipal(),身份信息,大部分情況下返回的是UserDetails接口的實現類,UserDetails代表用戶的詳細
信息,那從Authentication中取出來的UserDetails就是當前登錄用戶信息,它也是框架中的常用接口之一。

UserDetailService

DaoAuthenticationProvider中包含了一個UserDetailsService實例,它負責根據用戶名提取用戶信息 UserDetails(包含密碼),而後DaoAuthenticationProvider會去對比UserDetailsService提取的用戶密碼與用戶提交 的密碼是否匹配作爲認證成功的關鍵依據,因此可以通過將自定義的 UserDetailsService 公開爲spring bean來定 義自定義身份驗證。
public interface UserDetailsService { 
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 
}
很多人把DaoAuthenticationProviderUserDetailsService的職責搞混淆,其實UserDetailsService只負責從特定
的地方(通常是數據庫)加載用戶信息,僅此而已。而DaoAuthenticationProvider的職責更大,它完成完整的認
證流程,同時會把UserDetails填充至Authentication
public interface UserDetails extends Serializable { 
    Collection<? extends GrantedAuthority> getAuthorities(); 
    String getPassword(); 
    String getUsername(); 
    boolean isAccountNonExpired(); 
    boolean isAccountNonLocked(); 
    boolean isCredentialsNonExpired(); 
    boolean isEnabled(); 
}
它和Authentication接口很類似,比如它們都擁有usernameauthoritiesAuthenticationgetCredentials()
UserDetails中的getPassword()需要被區分對待,前者是用戶提交的密碼憑證,後者是用戶實際存儲的密碼,認證
其實就是對這兩者的比對。Authentication中的getAuthorities()實際是由UserDetailsgetAuthorities()傳遞而形
成的。還記得Authentication接口中的getDetails()方法嗎?其中的UserDetails用戶詳細信息便是經過了
AuthenticationProvider認證之後被填充的。
通過實現UserDetailsServiceUserDetails,我們可以完成對用戶信息獲取方式以及用戶信息字段的擴展。
Spring Security提供的InMemoryUserDetailsManager(內存認證)JdbcUserDetailsManager(jdbc認證)就是
UserDetailsService的實現類,主要區別無非就是從內存還是從數據庫加載用戶。

當我們查詢數據庫的時候,我們應該實現這個接口UserDetailService,重寫方法。就可以與我們的業務相關聯。我們的實體類也可以實現UserDetails這個接口,然後再加自己的屬性。

PasswordEncoder

DaoAuthenticationProvider認證處理器通過UserDetailsService獲取到UserDetails後,它是如何與請求 Authentication中的密碼做對比呢? 在這裏Spring Security爲了適應多種多樣的加密類型,又做了抽象,DaoAuthenticationProvider通過 PasswordEncoder接口的matches方法進行密碼的對比,而具體的密碼對比細節取決於實現
public interface PasswordEncoder { 
    String encode(CharSequence var1); 
    boolean matches(CharSequence var1, String var2); 
    default boolean upgradeEncoding(String encodedPassword) { return false; } 
}
Spring Security提供很多內置的PasswordEncoder,能夠開箱即用,使用某種PasswordEncoder只需要進行如
下聲明即可,如下
@Bean 
public PasswordEncoder passwordEncoder() { 
    return NoOpPasswordEncoder.getInstance(); 
}
NoOpPasswordEncoder採用字符串匹配方法,不對密碼進行加密比較處理,密碼比較流程如下:
 
(1)用戶輸入密碼(明文 )
(2)DaoAuthenticationProvider獲取UserDetails(其中存儲了用戶的正確密碼)
(3)DaoAuthenticationProvider使用PasswordEncoder對輸入的密碼和正確的密碼進行校驗,密碼一致則校驗通
過,否則校驗失敗。NoOpPasswordEncoder的校驗規則拿 輸入的密碼和UserDetails中的正確密碼進行字符串比較,字符串內容一致則校驗通過,否則 校驗失敗。

 

授權流程:

Spring Security可以通過 http.authorizeRequests() web請求進行授權保護。Spring Security使用標準Filter建立了對web請求的攔截,最終實現對資源的授權訪問。
 
分析授權流程:
1. 攔截請求,已認證用戶訪問受保護的web資源將被SecurityFilterChain中的 FilterSecurityInterceptor 的子
類攔截。
2. 獲取資源訪問策略FilterSecurityInterceptor會從 SecurityMetadataSource 的子類
DefaultFilterInvocationSecurityMetadataSource 獲取要訪問當前資源所需要的權限
Collection<ConfigAttribute>
SecurityMetadataSource其實就是讀取訪問策略的抽象,而讀取的內容,其實就是我們配置的訪問規則, 讀
取訪問策略如
http.authorizeRequests() .antMatchers("/r/r1").hasAuthority("p1") .antMatchers("/r/r2").hasAuthority("p2") ...
3. 最後,FilterSecurityInterceptor會調用 AccessDecisionManager 進行授權決策,若決策通過,則允許訪問資
源,否則將禁止訪問。
AccessDecisionManager(訪問決策管理器)的核心接口如下:
public interface AccessDecisionManager { 
    /** 
      * 通過傳遞的參數來決定用戶是否有訪問對應受保護資源的權限 
      */
    void decide(Authentication authentication , Object object, Collection<ConfigAttribute> configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException; 
//略.. 
}
這裏着重說明一下decide的參數:
authentication:要訪問資源的訪問者的身份
object:要訪問的受保護資源,web請求對應FilterInvocation
confifigAttributes:是受保護資源的訪問策略,通過SecurityMetadataSource獲取。
decide接口就是用來鑑定當前用戶是否有訪問對應受保護資源的權限。
 

授權決策

AccessDecisionManager採用投票的方式來確定是否能夠訪問受保護資源。
public interface AccessDecisionVoter<S> { 
    int ACCESS_GRANTED = 1; 
    int ACCESS_ABSTAIN = 0; 
    int ACCESS_DENIED = ‐1; 
    boolean supports(ConfigAttribute var1);     
    boolean supports(Class<?> var1); 
    int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3); 
}
vote()方法的返回結果會是AccessDecisionVoter中定義的三個常量之一。ACCESS_GRANTED表示同意, ACCESS_DENIED表示拒絕,ACCESS_ABSTAIN表示棄權。如果一個AccessDecisionVoter不能判定當前 Authentication是否擁有訪問對應受保護對象的權限,則其vote()方法的返回值應當爲棄權ACCESS_ABSTAIN
 
三個實現類:
 
1、AffiffiffirmativeBased:
1)只要有AccessDecisionVoter的投票爲ACCESS_GRANTED則同意用戶進行訪問;
2)如果全部棄權也表示通過;
3)如果沒有一個人投贊成票,但是有人投反對票,則將拋出AccessDeniedException
2、ConsensusBased:
1)如果贊成票多於反對票則表示通過。
2)反過來,如果反對票多於贊成票則將拋出AccessDeniedException
3)如果贊成票與反對票相同且不等於0,並且屬性allowIfEqualGrantedDeniedDecisions的值爲true,則表 示通過,否則將拋出異常AccessDeniedException。參數allowIfEqualGrantedDeniedDecisions的值默認爲true
4)如果所有的AccessDecisionVoter都棄權了,則將視參數allowIfAllAbstainDecisions的值而定,如果該值 爲true則表示通過,否則將拋出異常AccessDeniedException。參數allowIfAllAbstainDecisions的值默認爲false
3、UnanimousBased
的邏輯與另外兩種實現有點不一樣,另外兩種會一次性把受保護對象的配置屬性全部傳遞 給AccessDecisionVoter進行投票,而UnanimousBased會一次只傳遞一個ConfifigAttribute給 AccessDecisionVoter進行投票。這也就意味着如果我們的AccessDecisionVoter的邏輯是隻要傳遞進來的 ConfifigAttribute中有一個能夠匹配則投贊成票,但是放到UnanimousBased中其投票結果就不一定是贊成了。 UnanimousBased的邏輯具體來說是這樣的:
1)如果受保護對象配置的某一個ConfifigAttribute被任意的AccessDecisionVoter反對了,則將拋出
AccessDeniedException
2)如果沒有反對票,但是有贊成票,則表示通過。
3)如果全部棄權了,則將視參數allowIfAllAbstainDecisions的值而定,true則通過,false則拋出
AccessDeniedException
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章