天天看點

Spring Security(一)--Architecture Overview1 核心元件

一直以來我都想寫一寫Spring Security系列的文章,但是整個Spring Security體系強大卻又繁雜。陸陸續續從最開始的guides接觸它,到項目中看了一些源碼,到最近這個月為了寫一寫這個系列的文章,閱讀了好幾遍文檔,最終打算嘗試一下,寫一個較為完整的系列文章。

較為簡單或者體量較小的技術,完全可以參考着demo直接上手,但系統的學習一門技術則不然。以我的認知,一般的文檔大緻有兩種風格:Architecture First和Code First。前者緻力于讓讀者先了解整體的架構,友善我們對自己的認知有一個宏觀的把控,而後者以特定的demo配合講解,可以讓讀者在解決問題的過程中順便掌握一門技術。關注過我部落格或者公衆号的朋友會發現,我之前介紹技術的文章,大多數是Code First,提出一個需求,介紹一個思路,解決一個問題,分析一下源碼,大多如此。而學習一個體系的技術,我推薦Architecture First,正如本文标題所言,這篇文章是我Spring Security系列的第一篇,主要是根據Spring Security文檔選擇性翻譯整理而成的一個架構概覽,配合自己的一些注釋友善大家了解。寫作本系列文章時,參考版本為Spring Security 4.2.3.RELEASE。

https://blog.didispace.com/xjf-spring-security-1/#1-%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6 1 核心元件

這一節主要介紹一些在Spring Security中常見且核心的Java類,它們之間的依賴,建構起了整個架構。想要了解整個架構,最起碼得對這些類眼熟。

https://blog.didispace.com/xjf-spring-security-1/#1-1-SecurityContextHolder 1.1 SecurityContextHolder

SecurityContextHolder

用于存儲安全上下文(security context)的資訊。目前操作的使用者是誰,該使用者是否已經被認證,他擁有哪些角色權限…這些都被儲存在SecurityContextHolder中。

SecurityContextHolder

預設使用

ThreadLocal

政策來存儲認證資訊。看到

ThreadLocal

也就意味着,這是一種與線程綁定的政策。Spring Security在使用者登入時自動綁定認證資訊到目前線程,在使用者退出時,自動清除目前線程的認證資訊。但這一切的前提,是你在web場景下使用Spring Security,而如果是Swing界面,Spring也提供了支援,

SecurityContextHolder

的政策則需要被替換,鑒于我的初衷是基于web來介紹Spring Security,是以這裡以及後續,非web的相關的内容都一筆帶過。

https://blog.didispace.com/xjf-spring-security-1/#%E8%8E%B7%E5%8F%96%E5%BD%93%E5%89%8D%E7%94%A8%E6%88%B7%E7%9A%84%E4%BF%A1%E6%81%AF 擷取目前使用者的資訊

因為身份資訊是與線程綁定的,是以可以在程式的任何地方使用靜态方法擷取使用者資訊。一個典型的擷取目前登入使用者的姓名的例子如下所示:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}      

getAuthentication()傳回了認證資訊,再次getPrincipal()傳回了身份資訊,UserDetails便是Spring對身份資訊封裝的一個接口。Authentication和UserDetails的介紹在下面的小節具體講解,本節重要的内容是介紹SecurityContextHolder這個容器。

https://blog.didispace.com/xjf-spring-security-1/#1-2-Authentication 1.2 Authentication

先看看這個接口的源碼長什麼樣:

package org.springframework.security.core;// <1>

public interface Authentication extends Principal, Serializable { // <1>
    Collection<? extends GrantedAuthority> getAuthorities(); // <2>

    Object getCredentials();// <2>

    Object getDetails();// <2>

    Object getPrincipal();// <2>

    boolean isAuthenticated();// <2>

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}      

<1> Authentication是spring security包中的接口,直接繼承自Principal類,而Principal是位于

java.security

包中的。可以見得,Authentication在spring security中是最進階别的身份/認證的抽象。

<2> 由這個頂級接口,我們可以得到使用者擁有的權限資訊清單,密碼,使用者細節資訊,使用者身份資訊,認證資訊。

還記得1.1節中,authentication.getPrincipal()傳回了一個Object,我們将Principal強轉成了Spring Security中最常用的UserDetails,這在Spring Security中非常常見,接口傳回Object,使用instanceof判斷類型,強轉成對應的具體實作類。接口詳細解讀如下:

  • getAuthorities(),權限資訊清單,預設是GrantedAuthority接口的一些實作類,通常是代表權限資訊的一系列字元串。
  • getCredentials(),密碼資訊,使用者輸入的密碼字元串,在認證過後通常會被移除,用于保障安全。
  • getDetails(),細節資訊,web應用中的實作接口通常為 WebAuthenticationDetails,它記錄了通路者的ip位址和sessionId的值。
  • getPrincipal(),敲黑闆!!!最重要的身份資訊,大部分情況下傳回的是UserDetails接口的實作類,也是架構中的常用接口之一。UserDetails接口将會在下面的小節重點介紹。

https://blog.didispace.com/xjf-spring-security-1/#Spring-Security%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%8C%E6%88%90%E8%BA%AB%E4%BB%BD%E8%AE%A4%E8%AF%81%E7%9A%84%EF%BC%9F Spring Security是如何完成身份認證的?

1 使用者名和密碼被過濾器擷取到,封裝成

Authentication

,通常情況下是

UsernamePasswordAuthenticationToken

這個實作類。

2

AuthenticationManager

身份管理器負責驗證這個

Authentication

3 認證成功後,

AuthenticationManager

身份管理器傳回一個被填充滿了資訊的(包括上面提到的權限資訊,身份資訊,細節資訊,但密碼通常會被移除)

Authentication

執行個體。

4

SecurityContextHolder

安全上下文容器将第3步填充了資訊的

Authentication

,通過SecurityContextHolder.getContext().setAuthentication(…)方法,設定到其中。

這是一個抽象的認證流程,而整個過程中,如果不糾結于細節,其實隻剩下一個

AuthenticationManager

是我們沒有接觸過的了,這個身份管理器我們在後面的小節介紹。将上述的流程轉換成代碼,便是如下的流程:

public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();

public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while(true) {
    System.out.println("Please enter your username:");
    String name = in.readLine();
    System.out.println("Please enter your password:");
    String password = in.readLine();
    try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
    } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
    }
    }
    System.out.println("Successfully authenticated. Security context contains: " +
            SecurityContextHolder.getContext().getAuthentication());
}
}

class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}

public Authentication authenticate(Authentication auth) throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
    return new UsernamePasswordAuthenticationToken(auth.getName(),
        auth.getCredentials(), AUTHORITIES);
    }
    throw new BadCredentialsException("Bad Credentials");      

注意:上述這段代碼隻是為了讓大家了解Spring Security的工作流程而寫的,不是什麼源碼。在實際使用中,整個流程會變得更加的複雜,但是基本思想,和上述代碼如出一轍。

https://blog.didispace.com/xjf-spring-security-1/#1-3-AuthenticationManager 1.3 AuthenticationManager

初次接觸Spring Security的朋友相信會被

AuthenticationManager

ProviderManager

AuthenticationProvider

…這麼多相似的Spring認證類搞得暈頭轉向,但隻要稍微梳理一下就可以了解清楚它們的聯系和設計者的用意。AuthenticationManager(接口)是認證相關的核心接口,也是發起認證的出發點,因為在實際需求中,我們可能會允許使用者使用使用者名+密碼登入,同時允許使用者使用郵箱+密碼,手機号碼+密碼登入,甚至,可能允許使用者使用指紋登入(還有這樣的操作?沒想到吧),是以說AuthenticationManager一般不直接認證,AuthenticationManager接口的常用實作類

ProviderManager

内部會維護一個

List<AuthenticationProvider>

清單,存放多種認證方式,實際上這是委托者模式的應用(Delegate)。也就是說,核心的認證入口始終隻有一個:AuthenticationManager,不同的認證方式:使用者名+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機号碼+密碼登入則對應了三個AuthenticationProvider。這樣一來四不四就好了解多了?熟悉shiro的朋友可以把AuthenticationProvider了解成Realm。在預設政策下,隻需要通過一個AuthenticationProvider的認證,即可被認為是登入成功。

隻保留了關鍵認證部分的ProviderManager源碼:

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {

    // 維護一個AuthenticationProvider清單
    private List<AuthenticationProvider> providers = Collections.emptyList();
          
    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;

       // 依次認證
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {
             continue;
          }
          try {
             result = provider.authenticate(authentication);

             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有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;
    }
}      

ProviderManager

中的List,會依照次序去認證,認證成功則立即傳回,若認證失敗則傳回null,下一個AuthenticationProvider會繼續嘗試認證,如果所有認證器都無法認證成功,則

ProviderManager

會抛出一個ProviderNotFoundException異常。

到這裡,如果不糾結于AuthenticationProvider的實作細節以及安全相關的過濾器,認證相關的核心類其實都已經介紹完畢了:身份資訊的存放容器SecurityContextHolder,身份資訊的抽象Authentication,身份認證器AuthenticationManager及其認證流程。姑且在這裡做一個分隔線。下面來介紹下AuthenticationProvider接口的具體實作。

https://blog.didispace.com/xjf-spring-security-1/#1-4-DaoAuthenticationProvider 1.4 DaoAuthenticationProvider

AuthenticationProvider最最最常用的一個實作便是DaoAuthenticationProvider。顧名思義,Dao正是資料通路層的縮寫,也暗示了這個身份認證器的實作思路。由于本文是一個Overview,姑且隻給出其UML類圖:

按照我們最直覺的思路,怎麼去認證一個使用者呢?使用者前台送出了使用者名和密碼,而資料庫中儲存了使用者名和密碼,認證便是負責比對同一個使用者名,送出的密碼和儲存的密碼是否相同便是了。在Spring Security中。送出的使用者名和密碼,被封裝成了UsernamePasswordAuthenticationToken,而根據使用者名加載使用者的任務則是交給了UserDetailsService,在DaoAuthenticationProvider中,對應的方法便是retrieveUser,雖然有兩個參數,但是retrieveUser隻有第一個參數起主要作用,傳回一個UserDetails。還需要完成UsernamePasswordAuthenticationToken和UserDetails密碼的比對,這便是交給additionalAuthenticationChecks方法完成的,如果這個void方法沒有抛異常,則認為比對成功。比對密碼的過程,用到了PasswordEncoder和SaltSource,密碼加密和鹽的概念相信不用我贅述了,它們為保障安全而設計,都是比較基礎的概念。

如果你已經被這些概念搞得暈頭轉向了,不妨這麼了解DaoAuthenticationProvider:它擷取使用者送出的使用者名和密碼,比對其正确性,如果正确,傳回一個資料庫中的使用者資訊(假設使用者資訊被儲存在資料庫中)。

https://blog.didispace.com/xjf-spring-security-1/#1-5-UserDetails%E4%B8%8EUserDetailsService 1.5 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之後被填充的。

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

UserDetailsService和AuthenticationProvider兩者的職責常常被人們搞混,關于他們的問題在文檔的FAQ和issues中屢見不鮮。記住一點即可,敲黑闆!!!UserDetailsService隻負責從特定的地方(通常是資料庫)加載使用者資訊,僅此而已,記住這一點,可以避免走很多彎路。UserDetailsService常見的實作類有JdbcDaoImpl,InMemoryUserDetailsManager,前者從資料庫加載使用者,後者從記憶體中加載使用者,也可以自己實作UserDetailsService,通常這更加靈活。

https://blog.didispace.com/xjf-spring-security-1/#1-6-%E6%9E%B6%E6%9E%84%E6%A6%82%E8%A7%88%E5%9B%BE 1.6 架構概覽圖

為了更加形象的了解上述我介紹的這些核心類,附上一張按照我的了解,所畫出Spring Security的一張非典型的UML圖

如果對Spring Security的這些概念感到了解不能,不用擔心,因為這是Architecture First導緻的必然結果,先過個眼熟。後續的文章會秉持Code First的理念,陸續詳細地講解這些實作類的使用場景,源碼分析,以及最基本的:如何配置Spring Security,在後面的文章中可以不時翻看這篇文章,找到具體的類在整個架構中所處的位置,這也是本篇文章的定位。另外,一些Spring Security的過濾器還未囊括在架構概覽中,如将表單資訊包裝成UsernamePasswordAuthenticationToken的過濾器,考慮到這些雖然也是架構的一部分,但是真正重寫他們的可能性較小,是以打算放到後面的章節講解。