Spring Security小教程 Vol 2. Authentication核心組件介紹

噗噗虎和阿基拉的廢柴大叔小課堂

前言

上一期咱們介紹瞭如何最簡單的爲一個SpringBoot應用添加Spring Security框架,並使其爲應用完成用戶鑑權和訪問控制的受權服務。 這一期咱們將聚焦在用戶鑑權部分,用戶鑑權又能夠從框架核心和與Spring Web集成兩個角度切入,咱們選擇自底向上,先從框架核心領域開始討論,最後再對Spring Security是如何對Spring Web提供支持和服務進行展開。 爲了聚焦在覈心業務,一部分源代碼都通過必定處理,可能與框架自身代碼不一樣,目的是但願能夠更簡單的理解核心設計和相關類的職責和功能。java

第二期 Authentication核心簡介

本期的任務清單

  1. Authentication核心相關的組件和流程
  2. 基於用戶名和密碼的驗證流程演示

一、Authentication核心相關的組件和流程

Authentication即鑑權、通俗的說就是用戶登陸操做,在咱們通常的設計中,登陸提交和登陸成功獲取的都是用戶記錄信息;整個鑑權服務只提供一個驗證登陸的接口。 在這邊咱們先預設幾個問題,請你們帶着這幾個預設問題一邊瞭解核心組件一邊嘗試回答下這幾個問題:spring

  1. 登陸時提交的用戶名和密碼封裝在了哪一個類中?
  2. 哪一個接口負責接收登陸提交的用戶名和密碼,返回的又是什麼類,封裝哪些信息?
  3. 鑑權服務是如何判斷用戶名和密碼是否正確?又是如何獲取已有的用戶信息的?

首先,在咱們通常設計中能對於登陸信息的輸入會抽象爲一個表單或者更簡單的只是諸如User authentication(String username,String password)這樣的方法簽名進行處理。 而Spring Security中設計了一個\color{red}{Authentication}類,咱們輸入的用戶名和密碼被當作用戶的主體標識和用戶的鑑權憑證進行封裝。 數據庫

Authentication
其中Principal即是用戶名用於可惟一標識用戶信息的屬性,Credentials字段則存儲密碼做爲用戶端鑑權憑證。若是咱們不使用密碼而是使用其餘諸如令牌之類的也能夠將其視爲是的鑑權憑證。 不一樣的驗證協議可能存在不一樣的請求參數格式,Spring Security中也提供了常見的幾種常見的封裝,好比咱們以前說到的使用用戶名密碼和使用RememberMe的令牌形式。
Authentication相關類

除此之外,Authentication不只封裝用於登陸的屬性,在完成鑑權操做後也會將它關聯的權限列表所有存儲在Authorities中。整個鑑權的最初和最終狀態就比如咱們去食堂打飯拉卡,把飯盒和飯卡都遞給打飯阿姨,只要飯卡里有錢,阿姨就會把對應的飯盒給你裝滿。你遞出去是飯盒和飯卡,拿回來的仍是飯盒和飯卡。不管未來飯盒是兩層仍是三層,飯卡是實體卡仍是支付寶都和打飯的過程沒有了關係。緩存

而這個打飯的服務鑑權服務的形式即是bash

Authentication authenticate(Authentication authentication)
複製代碼

而給咱們提供最終鑑權服務的打飯阿姨,她的名字叫\color{red}{AuthenticationProvider}框架

AuthenticationProvider
咱們要提供一個完整的鑑權服務,咱們至少須要完成如下兩個任務:

  1. 在預置的用戶信息庫中查找出當前須要鑑權的用戶記錄;
  2. 驗證Authentication中的鑑權憑證和用戶名與預置查詢出來的是否一致。

在實際開發中,咱們的系統能支持多種鑑權實現,多是對比預置用戶密碼是否一致,多是對比一個令牌的值是否一致,也能夠簡單的認爲AuthenticationProvider實現的是如何進行身份驗證的服務,一般咱們也會稱爲「認證機制」。Spring Security自身已經提供了多種認證機制,看看下面羅列的類名大概也就知道對應什麼認證機制了:ide

  • OAuth2LoginAuthenticationProvider
  • CasAuthenticationProvider
  • JwtAuthenticationProvider
  • GoogleAccountsAuthenticationProvider 這些AuthenticationProvider中都有針對自身不一樣的認證機制的業務邏輯實現,上述的第二個任務,驗證Authentication的鑑權憑證。而這些AuthenticationProvider還都須要從預置的用戶信息庫中獲取預置的用戶信息才能完成比對工做。Spring Security中將獲取用戶預置的信息的服務,提到了\color{red}{UserDetailsService}中:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
複製代碼

而預置的用戶信息被封裝在了UserDetails的類中,UserDetails中主要包含的基本與輸入的Authentication差很少,包含了用戶名、密碼、權限列表等基礎的用戶信息屬性。 ui

UserDetails.java
對比AuthenticationProvider是解決了「如何驗證用戶輸入的用戶信息與預置身份信息是否一致」,UserDetailsService是解決了「從哪裏獲取要對比的預置身份信息記錄」。因此在實現UserDetailsService的時候咱們不用考慮驗證機制的具體實現,只要考慮從哪裏獲取用戶信息就能夠了。 若是是從緩存中可以使用CachingUserDetailsService,用戶信息如是基於LDAP的那麼就可使用LdapUserDetailsService,若是是基於Mybatis這種JDBC框架的就可使用JdbcUserDetailsManager。 一樣的若是你使用的是Spring Security中尚未提供的方式,好比使用JPA,那麼只要實現UserDetailService根據JPA的操做客製化一個新的實現類就能夠了。
各類UserDetailsService的實現

咱們已經大體瞭解了AuthenticationProvider和UserDetailService兩個類的主要責任和工做,讓咱們來考慮最後一個問題,UserDetailsService的服務返回的是UserDetails,AuthenticationProvider的服務返回的是Authentication。那麼這個將UserDetails封裝的成Authentication的邏輯是是誰負責的?在Sping Security中由於UserDetailsService只提供一個根據用戶名返回用戶信息的動做,其餘的責任跟他都沒有關係,怎麼將UserDetails組裝成Authentication進一步向調用者返回的工做也是由AuthenticationProvider完成的。 回到咱們最開始的那個食堂,咱們部分的窗口有些特殊的服務,好比有些窗口的阿姨會說英文支持英文點單,有些窗口的阿姨會使用支付寶結帳支持支付寶點單。雖然這些阿姨支持各類各樣的打飯的姿式,可是本質上仍是一手給錢一手給飯的打飯服務。 過了幾年,咱們的食堂打飯阿姨每位都多才多藝可是也頂不住大學擴招以後帶來了翻倍的學生。最近已經有學生反映若是上午最後一節課不逃課去打飯,那麼買到午餐就是下午1點的事情了。學校領導根據實際狀況反映決定之後每一個窗口配一組員工,一個阿姨負責拿飯拿菜,一個叔叔負責點單、刷卡和將飯菜裝入飯盒。 沒錯,那個負責拿飯拿菜的叔叔就是UserDetailService,而那個點菜、刷卡和裝飯菜的阿姨就是AuthenticationProvider。打飯的阿姨依舊支持各類多才多藝的學生的打飯姿式,而拿飯菜的叔叔兩眼只要看着食堂碩大的大鍋菜盤子就好了。 如此分工以後後的一個月,不只食堂的效率提升了、學生能夠在12點鐘準備買到午餐以外,廣大老師也反應上午最後一節課的出勤率也大大提升了。 最後咱們引入最後角色\color{red}{ProviderManager},ProviderManager中配置了各類鑑權服務了除了驗證用戶信息服務以外的配置。同時也ProviderManager也是AuthenticationManager接口的實現類,而AuthenticationManager是惟一貫外的提供的用戶驗證服務接口。AuthenticationManager自己並不實現驗證用戶身份的邏輯而是委託交由其配置的AuthenticationProvider去完成。 spa

ProviderManager

最後一次回到咱們的大學食堂,學校爲了方便各位宅男宅女們更加方便、快捷地能吃到午餐,未來點菜能夠直接經過食堂門口點菜機進行點單。學生們在點菜機上選擇本身喜歡飯菜,並刷卡並能夠在窗口拿到本身想要的食物。 本來像我這般胖子一頓午餐又想吃煲仔飯又想吃留學生窗口的披薩餅,我就要排兩次隊。而如今有了這臺點菜機以後,我就能夠一次性點上兩個窗口的飯菜,而後就在門口等着拿就好了。一樣食堂窗口排隊留學窗口一直冷冷清清的,留學生基本都是秒排拿飯,而粵菜窗口都要排到食堂門口去了,如今有了點菜機的出現多是自鴉片戰爭以來第一次實現了中外人權平等,你們都要老老實實在點菜機前排隊。食堂打飯阿姨不用再直接面對各類花式打飯姿式的學生了,只要看着點菜機系統上顯示打訂單而後和拿飯拿菜的叔叔配置裝飯盒,而後判斷飯卡是否是有效開展工做就能夠了,打飯阿姨們紛紛反映工做簡單了不少,晚上都有心情去跳廣場舞。 到這裏基本的Authentication部分的核心組件咱們都介紹完了,負責全局配置、直接面對調用者的\color{red}{AuthenticationManager}點菜機、負責完整驗證用戶信息服務的\color{red}{AuthenticationProvider}打飯阿姨、負責獲取預置用戶信息、從大鍋飯菜裏拿飯菜拿飯拿菜的\color{red}{UserDetailsService}叔叔。 下一部分咱們將經過簡單的Java實例來模擬咱們的食堂Spring Security的Authentication核心流程。設計

二、基於用戶名和密碼的驗證流程演示

首先,咱們先要組裝一個能夠提供驗證用戶的服務,那麼咱們只要須要一個AuthenticationProvider,而AuthenticationProvider自己不會獲取預置的用戶信息,因此咱們還須要給其配置一個UserDetailsService的實現組件。

//僱傭一組食堂打飯叔叔阿姨組合
        UserDetailsService userDetailsService = new InMemoryUserDetailsManager();
        AuthenticationProvider provider = new DaoAuthenticationProvider();
        //組裝完畢
        ((DaoAuthenticationProvider) provider).setUserDetailsService(userDetailsService);
複製代碼

咱們這邊使用了InMemoryUserDetailsManager基於內存管理的預置用戶身份信息管理和基於用戶名和密碼的DaoAuthenticationProvider來驗證咱們的用戶身份信息。

下面咱們建立咱們的第一條用戶身份信息,用戶名爲user,密碼爲password,角色爲USER的身份信息到內存中:

((InMemoryUserDetailsManager) userDetailsService)
.createUser(User
       .withUsername("user").password("password").roles("USER")
       .build());
複製代碼

而後咱們模擬第一個驗證請求,咱們建立了一個Authentication,其實現爲基於用戶名和密碼的UsernamePasswordAuthenticationToken。咱們須要驗證的用戶名爲user,密碼位password

Authentication authentication = new UsernamePasswordAuthenticationToken("user","password");
複製代碼

接着咱們調用驗證服務,並在控制檯打印

Authentication result = provider.authenticate(authentication);
        System.out.println(result);
複製代碼

在控制檯上中的日誌最終一段則會顯示驗證完成返回的Authentication已經正確的獲取了咱們以前預設的USER權限。

Granted Authorities: ROLE_USER
複製代碼

若是咱們輸入的用戶名和密碼不正確呢?讓咱們嘗試下面的代碼:

Authentication authentication = new UsernamePasswordAuthenticationToken("user","wrong");
        Authentication result = provider.authenticate(authentication);
        System.out.println(result);
複製代碼

針對輸入的Authentication的身份信息沒法與預置的身份信息進行匹配的場景下,Spring Security的AuthenticationProvider便會拋出org.springframework.security.authentication.BadCredentialsException的異經常使用於告之用戶名和密碼與UserDetailsService獲取的預置信息不匹配。

最後,咱們屏蔽咱們與AuthenticationProvider的直連聯繫,咱們使用AuthenticationProvider來爲咱們提供多種Provider服務。在初始化ProviderManager的時候,咱們須要設置Manager所管理的全部AuthenticationProvider的列表,而後同樣調用authenticate驗證方法也能夠達到一樣的效果。

List<AuthenticationProvider> providers = new ArrayList<>();
        providers.add(provider);
       AuthenticationManager manager = new ProviderManager(providers);
        Authentication result = manager .authenticate(authentication);
        System.out.println(result);
複製代碼

那麼爲何還要額外有一層AuthenticationManager 的封裝?請讓咱們考慮下如下的場景,若是咱們的系統又支持基於用戶和密碼的服務,其數據是從數據庫中獲取的,同時又支持使用Token從Redis中驗證Token的用戶驗證服務。在這樣的場景下,咱們便只要額外在AuthenticationManager的AuthenticationProvider列表增長一個基於Token驗證的AuthenticationProvider(它的UserDetailsService則是經過Redis的API實現)即可了。這樣的配置修改並不會影響到核心對外部暴露的服務接口和相關Authentication參數。

結尾

通過了大學食堂的洗禮,咱們基本上已經對Spring Security中Authentication鑑權(身份驗證)服務端核心組件和主要流程進行了說明,下一期咱們將對Spring Security是如何對Spring Web應用提供支持的機制進行說明。

相關文章
相關標籤/搜索