天天看點

Security認證流程分析

一、security原理

security是通過一系列filter完成認證和授權的

Security認證流程分析

security預設提供了30多個過濾器,spring boot在對security進行自動化配置時,會建立一個名為springsecuritychian過濾鍊并注入到spring容器中,filterchianproxy做為頂層管理者統一管理springsecuritychian,filterchianproxy本身通過DelegatinFilterProxy整合到原生web過濾鍊中

過濾器清單

過濾器 作用 預設開啟
ChannelProcessingFilter 過濾請求協定 https http 預設no 過濾請求協定 https http no
WebAsyncManagerIntegrationFilter 将WebAsyncManager和security上下文結合 yes
SecurityContextPersistenceFilter 處理請求前将安全新加載到securitycontextholder yes
HeaderWriterFilter 處理頭資訊加入到響應中 yes
CorsFilter 處理跨域問題 no
CsrfFilter 處理csrf攻擊 yes
LogoutFilter 處理登出登入 yes
OAuth2AuthorizationRequestRedirectFilter 處理outh2認證重定向 no
Saml2WebSsoAuthenticationRequestFilter 處理saml認證 no
X509AuthenticationFilter 處理x509認證 no
AbstractPreAuthenticatedProcessingFilter 處理預認證問題 no
CasAuthenticationFilter 處理cas認證 no
OAuth2LoginAuthenticationFilter 處理oauth2認證 no
Saml2WebSsoAuthenticationFilter 處理saml認證 no
​​UsernamePasswordAuthenticationFilter​​ 處理表單認證 yes
OpenIDAuthenticationFilter 處理openid認證 no
DefaultLoginPageGeneratingFilter 配置預設登入界面 yes
DefaultLogoutPageGeneratingFilter 預設登出頁面 yes
ConcurrentSessionFilter 處理session有效期 no
​​DigestAuthenticationFilter​​ 處理http摘要認證 no
BearerTokenAuthenticationFilter 處理oauth2的accesstoken no
​​BasicAuthenticationFilter​​ 處理httpbasic認證 yes
RequestCacheAwareFilter 處理請求緩存 yes
SecurityContextHolderAwareRequestFilter 包裝原始請求 yes
JaasApiIntegrationFilter 處理jaas認證 no
RememberMeAuthenticationFilter 處理rememberme登入 no
AnonymousAuthenticationFilter 配置匿名認證 yes
OAuth2AuthorizationCodeGrantFilter 處理oauth2認證授權碼 no
SessionManagementFilter 處理session并發問題 yes
​​ExceptionTranslationFilter​​ 處理認證授權中的異常 yes
​​FilterSecurityInterceptor​​ 處理授權相關 yes
SwitchUserFilter 處理賬戶切換 no

二、認證流程分析

1、前端輸入完使用者名密碼之後,會進入UsernamePasswordAuthenticationFilter類中去擷取使用者名和密碼,然後去建構一個UsernamePasswordAuthenticationToken對象。

UsernamePasswordAuthenticationToken這個類是實作了Authentication接口,在調用UsernamePasswordAuthenticationToken的構造函數的時候先調用父類AbstractAuthenticationToken的構造方法,傳遞一個null,因為在認證的時候并不知道這個使用者有什麼權限,之後去給使用者名密碼指派,最後有一個setAuthenticated(false)方法,代表未認證,源碼如下:

Security認證流程分析

執行個體化UsernamePasswordAuthenticationToken之後調用了setDetails(request,authRequest)将請求的資訊設到UsernamePasswordAuthenticationToken中去,包括ip、session等内容

 2、然後執行

this.getAuthenticationManager().authenticate(authRequest)      

AuthenticationManager本身不包含驗證的邏輯,它的作用是管理AuthenticationProvider。

3、authenticate這個方法是在ProviderManager類上的,這個類實作了AuthenticationManager接口,在authenticate方法中有一個for循環,拿到所有的AuthenticationProvider,真正校驗的邏輯是寫在AuthenticationProvider中的,為什麼是一個集合去進行循環?是因為不同的登陸方式認證邏輯是不一樣的,可能是微信等社交平台登陸,也可能是使用者名密碼登陸。AuthenticationManager其實是将AuthenticationProvider收集起來,然後登陸的時候挨個去AuthenticationProvider中問你這種驗證邏輯支不支援此次登陸的方式,根據傳進來的Authentication類型會挑出一個适合的provider來進行校驗處理。

然後去調用provider的驗證方法authenticate方法,authenticate是DaoAuthenticationProvider類中的一個方法,DaoAuthenticationProvider繼承了AbstractUserDetailsAuthenticationProvider。實際上authenticate的校驗邏輯寫在了AbstractUserDetailsAuthenticationProvider抽象類中,首先執行個體化UserDetails對象,調用了retrieveUser方法擷取到了一個user對象,retrieveUser是一個抽象方法。

AbstractUserDetailsAuthenticationProvider的 authenticate 方法的一部分:

Security認證流程分析

注意:如果自己自定義了一個MyAuthenticationProvider繼承了AuthenticationProvider,這裡就會走自己的認證類。

4、DaoAuthenticationProvider實作了 retrieveUser 方法,在實作的方法中執行個體化了UserDetails對象

Security認證流程分析

注意:如果自己自定義了UserDetailsServiceImpl 實作了 UserDetailsService就會走自己的方法。

5、也就是相當于自定義驗證邏輯的那個類,去實作UserDetailService類,這個傳回結果就是我們自己在資料庫中根據username查詢出來的使用者資訊。在AbstractUserDetailsAuthenticationProvider中如果沒拿到資訊就會抛出異常,如果查到了就會去調用preAuthenticationChecks的check方法去進行預檢查。

AbstractUserDetailsAuthenticationProvider的 authenticate 方法的一部分:

在預檢查中進行了三個檢查,因為UserDetail類中有四個布爾類型,去檢查其中的三個,使用者是否鎖定、使用者是否過期,使用者是否可用。

預檢查之後緊接着去調用了additionalAuthenticationChecks方法去進行附加檢查,這個方法也是一個抽象方法,在DaoAuthenticationProvider中去具體實作,在裡面進行了加密解密去校驗目前的密碼是否比對。比對密碼的過程,用到了PasswordEncoder和SaltSource,密碼加密和鹽的概念。

如果通過了預檢查和附加檢查,還會進行厚檢查,檢查4個布爾中的最後一個。

所有的檢查都通過,則認為使用者認證是成功的。使用者認證成功之後,會将這些認證資訊和user傳遞進去,調用createSuccessAuthentication方法。

createSuccessAuthentication方法中同樣會執行個體化一個user,但是這個方法不會調用之前傳兩個參數的函數,而是會調用三個參數的構造函數。這個時候,在調super的構造函數中不會再傳null,會将authorities權限設進去,之後将使用者密碼設進去,最後setAuthenticated(true),代表驗證已經通過。

Security認證流程分析

最後建立一個authentication會沿着驗證的這條線傳回回去。如果驗證成功,則在這條路中調用我們系統的業務邏輯。如果在任何一處發生問題,就會抛出異常,調用我們自己定義的認證失敗的處理器。 

三、總結

1、UserDetails與UserDetailsService差別

UserDetails這個接口,它代表了最詳細的使用者資訊,這個接口涵蓋了一些必要的使用者資訊字段,具體的實作類對它進行了擴充。

public interface UserDetails extends Serializable {

   Collection<? extends GrantedAuthority> getAuthorities();

   String getPassword();

   String getUsername();

   boolean isAccountNonExpired();

   boolean isAccountNonLocked();

   boolean isCredentialsNonExpired();

   boolean isEnabled();
}      

它和Authentication接口很類似,比如它們都擁有username,authorities,區分他們也是本文的重點内容之一。Authentication的getCredentials()與UserDetails中的getPassword()需要被區分對待,前者是使用者送出的密碼憑證,後者是使用者正确的密碼,認證器其實就是對這兩者的比對。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而形成的。還記得Authentication接口中的getUserDetails()方法嗎?其中的UserDetails使用者詳細資訊便是經過了AuthenticationProvider之後被填充的。!UserDetailsService隻負責從特定的地方(通常是資料庫)加載使用者資訊,僅此而已,記住這一點,可以避免走很多彎路

public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}      

2、authenticationmanager ,providermanager, authenticationprovider關系

security中支援多種不同的認證方式,比如使用者名和密碼認證、手機号驗證碼認證等,不同的認證方式對應了不同authenticationprovider是以一個完整的流程可能由多個authenticationprovider提供,多個authenticationprovider組成一個清單,這個清單将由providermanager代理,換句話說providermanager中存在一個清單,在providermanager中周遊清單中每一個authenticationprovider去執行身份認證,最終得到認證結果。

providermanager本身也可以再配置一個authenticationmanager作為parent,這樣當providermanager認證失敗就可以進入到parent中再次認證,理論上providermanager的parent可以是任意類型的authenticationmanager,但是通常由providermanager來扮演parent角色,也就是providermanager是providermanager的parent。

providermanager本身也可以有多個,多個providermanager共用一個parent,有時一個應用程式有受保護資源的邏輯組,比如所有符合路徑模式的資源/api/**.每個組可以有自己專用的authenticationmanager,通常每個組都是一個providermanager,他們共享一個父級,然後父級是一種全局資源,作為所有提供者的後備資源,由此三者關系如圖

Security認證流程分析

3、使用者資訊擷取

3.1、業務代碼中擷取

security會将登入的使用者資訊資料儲存在session中,security在此基礎上做了改進,通過一個線程将登陸成的資訊儲存到SecurityContextHolder中,

SecurityContextHolder中資料儲存預設是通過Theadlocal實作的,使用Theadlocal建立的變量隻能通過目前線程通路,不能被其他線程或者 目前線程的子線程通路,就是使用者資料和請求線程是綁定在一起的,請求完成後,security會将在session中登入資訊清空,以後有請求時security會從session資訊儲存到SecurityContextHolder友善該請求後續處理

實際上SecurityContextHolder存儲的是SecurityContext ,在SecurityContext存儲的是Authentication

Security認證流程分析

SecurityContextHolder源碼如下

public class SecurityContextHolder {
    //存儲政策是隻有目前請求線程才能擷取使用者資訊 開啟子線程也是無法拿到使用者資訊
    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    //多線程存儲政策 如果業務子線程中也可以擷取使用者資訊 使用這個
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    //是将資料儲存到一個靜态變量中
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    private static String strategyName = System.getProperty("spring.security.strategy");
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;
}      

SecurityContentHolderStrategy源碼如下

public interface SecurityContextHolderStrategy {

  /**
   * 清除存儲的securityContext
   */
  void clearContext();

  /**
   * 擷取存儲的securityContext
   */
  SecurityContext getContext();

  /**
   *設定存儲的securityContext
   */
  void setContext(SecurityContext context);

  /**
   * 建立一個空的存儲的securityContext
   */
  SecurityContext createEmptyContext();

}      

代碼中擷取

@GetMapping("/index")
    public String index(){
       Authentication authentication =  SecurityContextHolder.getContext().getAuthentication();
       User user =  (User)authentication.getPrincipal();
       //身份資訊
        user.getUsername();
        //權限資訊
        user.getAuthorities();
        return "index";
    }      

其他方式

方法二:
 @GetMapping("/me")
    public Object getCurrentUser(Authentication authentication){
        return authentication;
    }
 
方法三:
@GetMapping("/me")
    public Object getCurrentUser(@AuthenticationPrincipal UserDetails userDetails){
        return userDetails;
    }      

3.2、頁面中擷取

<!--thymeleaf與security整合包-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>
        <!--thymeleaf啟動器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>      
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    登入賬号:<span sec:authentication="name">123</span><br/>
    登入賬号:<span sec:authentication="principal.username">456</span><br/>
    憑證:<span sec:authentication="credentials">456</span><br/>
    權限和角色:<span sec:authentication="authorities">456</span><br/>
    用戶端位址:<span sec:authentication="details.remoteAddress">456</span><br/>
    sessionId:<span sec:authentication="details.sessionId">456</span><br/>
</body>
</html>