Spring Security 中的身份認證

原文連接:https://blog.gaoyuexiang.cn/2020/06/07/spring-security-authentication/,內容無差異。java

本文介紹 Spring Security 的身份認證的內容,研究 Spring Security 自帶的身份認證方式和添加本身的身份認證方式的方法。spring

身份認證相關組件

上一篇文章中,咱們瞭解到了 Spring Security 會將 DelegatingFilterProxy 插入到 Servlet Filter Chain 中,而後將要過濾的請求經過 FilterChainProxy 代理給匹配的 SecurityFilterChain;這些 SecurityFilterChain 中包含着真正作安全相關工做的 Filter數據庫

後面提到的 Filter 都是紅色方框中的

這些 Filter 中的一部分,他們的職責就是進行身份驗證,好比 UsernamePasswordAuthenticationFilter;而他們中的大多數都有一個共同的父類 AbstractAuthenticationProcessingFiltersegmentfault

AbstractAuthenticationProcessFilter

這個類是不少身份認證的 Filter 的父類,它已經實現了 doFilter 方法,流程以下:安全

本文不涉及其中的 sessionStrategy 部分

doFilter 已經幫咱們搭好了這個流程,咱們只須要關心其中的幾個被調用的方法(紅綠藍三個顏色框)就能夠了。session

attemptAuthentication

這是一個抽象方法。子類實現的時候,須要從 HttpServletRequest 中獲取須要的信息,構建出一個 Authentication 實例,而後調用父類中的 AuthenticationManager.authenticate() 方法,對 Authentication 對象進行認證。mvc

unsuccessfulAuthentication

這個方法已經被實現,子類也能夠選擇重寫。根據父類的實現,這個方法將完成一下步驟:ide

  1. 清理 SecurityContextHolder
  2. 清除 RememberMeService 中的信息(默認使用 NullRememberMeService
  3. 調用 AuthenticationFailureHandler.onAuthenticationFailure() 方法

默認使用的 AuthenticationFailureHandlerSimpleUrlAuthenticationFailureHandler,它的邏輯是:spring-boot

  1. 若是沒有配置 defaultFailureUrl (默認沒有)網站

    1. 發送 401 響應
  2. 根據配置的布爾值 forwardToDestination (默認爲 false)判斷

    1. 使用 Servlet forward 到配置的 defaultFailureUrl
    2. 使用 HTTP redirect 到配置的 defaultFailureUrl

successfulAuthentication

unsuccessfulAuthentication 方法同樣,這個方法也已經實現,而且能夠被重寫,但其中的邏輯卻剛好相反:

  1. attemptAuthentication 返回的 Authentication 對象保存到 SecurityContextHolder
  2. 保存登錄信息到 RememberMeService
  3. 發佈 InteractiveAuthenticationSuccessEvent 事件,這樣能夠被配置的 EventListener 處理
  4. 調用 AuthenticationSuccessHandler.onAuthenticationSuccess() 方法

默認使用的 AuthenticationSuccessHandlerSavedRequestAwareAuthenticationSuccessHandler,其實現就是一次重定向。咱們能夠看看它重定向到哪裏:

  • 當配置了 alwaysUseDefaultTargetUrl 或指定了 targetUrlParameter 且此參數存在的時候

    • 若是配置了 alwaysUseDefaultTargetUrl 則重定向到 defaultTargetUrl,默認是 /
    • 若是存在 targetUrlParameter(好比 redirect_uri 之類的比較常見的參數),則重定向到這個路徑
    • 若是存在 Referer,則重定向到這個地址
    • 重定向到 /
  • RequestCache 中找到了保存的請求

    • 重定向到請求中設置的重定向地址
  • 若是仍是沒有知足條件,則進行第一步裏的邏輯
關於 RequestCache:想象你正在訪問一個須要認證的資源,這個時候網站會把你重定向到登錄頁面;在你登錄成功後,又會重定向回剛纔的資源。 RequestCache 就是爲了保存登錄以前的請求而設計的。在這裏,默認使用基於 session 的實現。

AuthenticationManager

AbstractAuthenticationProcessingFilter 中保存了一個 AuthenticationManager,它會在子類的 attemptAuthentication 方法中被使用。其職責是對 Filter 建立的 Authentication 對象進行身份驗證,好比查詢數據庫匹配用戶名密碼、攜帶的 token 是否合法等。

ProviderManager 與 AuthenticationProvider

這是 AuthenticationManager 經常使用的實現。它沒有實現任何認證邏輯,而是管理了一些 AuthenticationProvider,經過這些 provider 來實現真正的認證功能。

每一個 AuthenticationProvider 實現一種特定類型的身份認證方式,好比用戶名密碼登錄、OIDC 登錄等。他們能夠經過 Authentication 的具體類型來判斷是否支持這種 Authentication 須要的認證方式。

其餘的一些 Filter

除了 AbstractAuthenticationProcessFilter,還有一些進行身份驗證的 Filter,它們並無繼承這個類,而是基於 OncePerRequestFilter 本身實現了一套邏輯。這些 Filter 包括 AuthenticationFilterBasicAuthenticationFilterOAuth2AuthorizationCodeGrantFilter 等等。

因爲它們再也不是 AbstractAuthenticationProcessFilter,因此不會再被要求使用 AuthenticationManager。儘管這樣,當咱們選擇使用 OncePerReuqestFilter 來實現自定義的身份認證時,仍然能夠考慮使用 AuthenticationManager 這種方式。

我的以爲 AuthenticationManager 還算是個不錯的設計,由於作到了職責分離。

甚至還有更加放飛自個人 DigestAuthenticationFilter,直接繼承 GenericFilterBean,在實現上也是我行我素,這裏就不探究了。

ExceptionTranslationFilter

這個 Filter 不是用來進行身份驗證的,而是用來處理認證受權過程當中產生的異常的。它能夠處理 AuthenticationExceptionAccessDeniedException,分別表示認證失敗和受權失敗。這篇文章只關心如何處理 AuthenticationException

可是這個 Filter 默認被安排在 SecurityFilterChain 的倒數第二位,因此前面的 Filter 拋出的異常並不能被它捕獲。但自定義的 Filter 能夠加到它後面,這樣就能夠利用它來處理這兩種異常。

最後一位是 FilterSecurityInterceptor,可能會拋出 AccessDeniedException

處理 AuthenticationException

ExceptionTranslationFilterAuthenticationException 的處理分三步:

  1. 清理 SecurityContextHolder 中的身份信息
  2. 將當前的 request、response 保存到 RequestCache 中(用途能夠回顧一下 successfulAuthentication 方法
  3. 調用 AuthenticationEntryPoint.commence() 方法

其中的 AuthenticationEntryPoint 具體實例取決於你的配置,默認會用到 BasicAuthenticationEntryPoint。這個接口的職責就是經過 WWW-Authenticate header 告訴客戶端使用哪一種方式進行身份驗證。

處理其餘異常

對於 AuthenticationExceptionAccessDeniedException 以外的異常,ExceptionTranslationFilter 會將其轉換成 ServletExceptionRuntimeException 拋出。

若是想要處理這些異常,須要本身添加 Filter 實現。

Spring Security 自動配置的 FilterChainProxy

當咱們啓動 Spring 應用以後,會在日誌裏看到打印全部配置的 FilterChainProxy

默認狀況下,咱們會看到這樣的一條鏈:

這是引入 spring-boot-starter-security 以後自動配置的 FilterChainProxy,在引入更多的 security 相關的依賴和編寫了相關配置以後,這個 filter chain 也會相應變化。

幾種內置的身份認證方式

接下來,咱們以 UsernamePawwrodAuthenticationFilterBasicAuthenticationFilter 爲例,看看他們是如何實現身份認證的。

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 是一個 AbstractAuthenticationProcessingFilter 的子類,實現了 attemptAuthentication 方法,沒有重寫其餘方法。因此用戶認證成功後,會被重定向到一個地址,具體邏輯參考上面的 successfulAuthentication 方法

attemptAuthentication

attemptAuthentication 方法會從 HttpServletRequest.getParameter() 方法中獲取用戶名密碼,從而進行身份驗證。具體從哪裏獲取用戶名密碼,則能夠被子類經過重寫 obtainUsername()obtainPassword() 方法修改。

以後,UsernamePasswordAuthenticationFilter 會構建出一個 UsernameAuthenticationToken,交給 AuthenticationManager 進行認證。

DaoAuthenticationProvider

這是 UsernamePasswordAuthenticationFilter 對應的 AuthenticationProvider,負責對 UsernameAuthenticationToken 進行認證。

它使用一個 UserDetailsService 來加載用戶信息,使用 PasswordEncoder 來匹配用戶的密碼。

這兩個接口具體使用哪個實現,取決於具體的配置。好比 UserDetailsService 就有 in memory 和 JDBC 的實現。

UsernamePasswordAuthenticationFilter 是用於單獨處理登陸的 Filter,它不是用來在請求業務 API 時進行身份認證的 Filter

事實上,全部繼承了 AbstractAuthenticationProcessFilter 但沒有重寫 successfulAuthentication 方法的 Filter 都是這樣的,它們會在登錄成功後重定向到登陸前的地址或默認的地址。這也符合它的語義:進行身份認證流程,而不是業務請求的一部分。

BasicAuthenticationFilter

UsernamePasswordAuthenticationFilter 不一樣,BasicAuthenticationFilter 沒有繼承 AbstractAuthenticationProcessingFilter,而是直接繼承 OncePerRequestFilter。由於它是被使用在請求業務 API 的請求上,而不是進行身份認證流程。

BasicAuthenticationFilter 的實現並不複雜,無非是從 Authorization header 中取出用戶名密碼,而後建立出 UsernameAuthenticationToken,接着調用 AuthenticationManager.authenticate() 方法。

之因此它也會使用 AuthenticationManager,應該是出於複用的考慮。這樣它就可使用和 UsernamePasswordAuthenticationFilter 同樣的 AuthenticationProvider

它與 UsernamePasswordAuthenticationFilter 的區別在於認證以後的行爲。

不管認證成功與否,BasicAuthenticationFilter 都不會作出重定向的響應。

  • 若是認證失敗,則經過默認的 BasicAuthenticationEntryPoint 返回 401 響應
  • 若是認證成功,則繼續執行 filter chain,這樣就能執行到真正的業務方法

如何添加本身的身份認證方式

前面介紹了兩種不一樣的 Filter 實現,以及它們被使用的場景,如今咱們知道了該選擇哪種方式去實現自定義的 Filter。可是,如何把它們加入到 SecurityFilterChain 中去處理身份認證呢?

配置 SecurityFilterChain

咱們若是須要任何對 SecurityFilterChain 的配置,都須要擴展 WebSecurityConfigurerAdapter,實現本身的一個配置類。每建立這樣的一個實現,都會建立一個 SecurityFilterChain 加入到 FilterChainProxy 中。

配置 requestMathcer

咱們在前一篇文章提到過,FilterChainProxy 須要根據 url 來判斷選擇哪個 SecurityFilterChain。咱們須要將這個配置寫到這個實現類中,好比:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.requestMatchers(matcher -> matcher.mvcMatchers("/hello"));
}

這樣,FilterChainProxy 就知道了對 /hello 的請求須要使用這個 SecurityFilterChain

向 SecurityFilterChain 加入 Filter

如今有了對應的 SecurityFilterChain,咱們就能夠將自定義的 Filter 加入到這個 chain 中:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.addFilter(HelloFilter.class);
}

addFilter 方法也有一些變體,能夠控制 Filter 在 chain 中的位置,這裏就不贅述了。

添加 AuthenticationProvider

Filter 同樣,AuthenticationProvider 也是被安排到單獨的 FilterChainProxy 中的,而且須要本身配置。若是你的自定義 Filter 須要 AuthenticationProvider 的話,一樣須要配置:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth.authenticationProvider(HelloAuthenticationProvider.class);
}

總結

這篇文章比較詳細的梳理了 AbstractAuthenticationProcessingFilter 及其子類 UsernamePasswordAuthenticationFilter 的實現和 BasicAuthenticationFilter 的實現,瞭解了須要實現自定義身份驗證的 Filter 時應該選擇哪一種方式:

  • 只是進行身份驗證,完成後進行重定向,而不調用業務方法,那麼就繼承 AbstractAuthenticationProcessFilter
  • 須要調用業務方法,身份驗證是爲了保護業務,那麼就繼承 OncePerRequestFilter,徹底控制認證的流程
固然,這不是一個強制的限制,你仍然能夠經過重寫 AbstractAuthenticationProcessFilter.successfulAuthentication() 方法來修改重定向的行爲。

另外,也瞭解到了實現完 Filter 後,須要實現 WebSecurityConfigurerAdapter,將 Filter 加入到 SecurityFilterChain 中。

👉 查看系列文章
相關文章
相關標籤/搜索