Spring Security(一) —— Architecture Overview

摘要: 原創出處 https://www.cnkirito.moe/spring-security-1/ 「老徐」歡迎轉載,保留摘要,謝謝!java



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

較爲簡單或者體量較小的技術,徹底能夠參考着demo直接上手,但系統的學習一門技術則否則。以個人認知,通常的文檔大體有兩種風格:Architecture First和Code First。前者致力於讓讀者先了解總體的架構,方便咱們對本身的認知有一個宏觀的把控,然後者以特定的demo配合講解,可讓讀者在解決問題的過程當中順便掌握一門技術。關注過我博客或者公衆號的朋友會發現,我以前介紹技術的文章,大多數是Code First,提出一個需求,介紹一個思路,解決一個問題,分析一下源碼,大多如此。而學習一個體系的技術,我推薦Architecture First,正如本文標題所言,這篇文章是我Spring Security系列的第一篇,主要是根據Spring Security文檔選擇性翻譯整理而成的一個架構概覽,配合本身的一些註釋方便你們理解。寫做本系列文章時,參考版本爲Spring Security 4.2.3.RELEASE。spring

1 核心組件

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

1.1 SecurityContextHolder

SecurityContextHolder用於存儲安全上下文(security context)的信息。當前操做的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權限…這些都被保存在SecurityContextHolder中。SecurityContextHolder默認使用ThreadLocal 策略來存儲認證信息。看到ThreadLocal 也就意味着,這是一種與線程綁定的策略。Spring Security在用戶登陸時自動綁定認證信息到當前線程,在用戶退出時,自動清除當前線程的認證信息。但這一切的前提,是你在web場景下使用Spring Security,而若是是Swing界面,Spring也提供了支持,SecurityContextHolder的策略則須要被替換,鑑於個人初衷是基於web來介紹Spring Security,因此這裏以及後續,非web的相關的內容都一筆帶過。安全

獲取當前用戶的信息

由於身份信息是與線程綁定的,因此能夠在程序的任何地方使用靜態方法獲取用戶信息。一個典型的獲取當前登陸用戶的姓名的例子以下所示:session

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

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

 

1.2 AuthenticationgetAuthentication()返回了認證信息,再次getPrincipal()返回了身份信息,UserDetails即是Spring對身份信息封裝的一個接口。Authentication和UserDetails的介紹在下面的小節具體講解,本節重要的內容是介紹SecurityContextHolder這個容器。架構

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

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;
}

 

<2> 由這個頂級接口,咱們能夠獲得用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息。<1> Authentication是spring security包中的接口,直接繼承自Principal類,而Principal是位於java.security包中的。能夠見得,Authentication在spring security中是最高級別的身份/認證的抽象。ide

還記得1.1節中,authentication.getPrincipal()返回了一個Object,咱們將Principal強轉成了Spring Security中最經常使用的UserDetails,這在Spring Security中很是常見,接口返回Object,使用instanceof判斷類型,強轉成對應的具體實現類。接口詳細解讀以下:源碼分析

  • getAuthorities(),權限信息列表,默認是GrantedAuthority接口的一些實現類,一般是表明權限信息的一系列字符串。
  • getCredentials(),密碼信息,用戶輸入的密碼字符串,在認證事後一般會被移除,用於保障安全。
  • getDetails(),細節信息,web應用中的實現接口一般爲 WebAuthenticationDetails,它記錄了訪問者的ip地址和sessionId的值。
  • getPrincipal(),敲黑板!!!最重要的身份信息,大部分狀況下返回的是UserDetails接口的實現類,也是框架中的經常使用接口之一。UserDetails接口將會在下面的小節重點介紹。

Spring Security是如何完成身份認證的?

1 用戶名和密碼被過濾器獲取到,封裝成Authentication,一般狀況下是UsernamePasswordAuthenticationToken這個實現類。

AuthenticationManager 身份管理器負責驗證這個Authentication

3 認證成功後,AuthenticationManager身份管理器返回一個被填充滿了信息的(包括上面提到的權限信息,身份信息,細節信息,但密碼一般會被移除)Authentication實例。

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");
}
}

 

1.3 AuthenticationManager注意:上述這段代碼只是爲了讓你們瞭解Spring Security的工做流程而寫的,不是什麼源碼。在實際使用中,整個流程會變得更加的複雜,可是基本思想,和上述代碼一模一樣。

初次接觸Spring Security的朋友相信會被AuthenticationManagerProviderManager ,AuthenticationProvider …這麼多類似的Spring認證類搞得暈頭轉向,但只要稍微梳理一下就能夠理解清楚它們的聯繫和設計者的用意。AuthenticationManager(接口)是認證相關的核心接口,也是發起認證的出發點,由於在實際需求中,咱們可能會容許用戶使用用戶名+密碼登陸,同時容許用戶使用郵箱+密碼,手機號碼+密碼登陸,甚至,可能容許用戶使用指紋登陸(還有這樣的操做?沒想到吧),因此說AuthenticationManager通常不直接認證,AuthenticationManager接口的經常使用實現類ProviderManager 內部會維護一個List<AuthenticationProvider>列表,存放多種認證方式,實際上這是委託者模式的應用(Delegate)。也就是說,核心的認證入口始終只有一個:AuthenticationManager,不一樣的認證方式:用戶名+密碼(UsernamePasswordAuthenticationToken),郵箱+密碼,手機號碼+密碼登陸則對應了三個AuthenticationProvider。這樣一來四不四就好理解多了?熟悉shiro的朋友能夠把AuthenticationProvider理解成Realm。在默認策略下,只須要經過一個AuthenticationProvider的認證,便可被認爲是登陸成功。

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

 1 public class ProviderManager implements AuthenticationManager, MessageSourceAware,
 2         InitializingBean {
 3 
 4     // 維護一個AuthenticationProvider列表
 5     private List<AuthenticationProvider> providers = Collections.emptyList();
 6 
 7     public Authentication authenticate(Authentication authentication)
 8           throws AuthenticationException {
 9        Class<? extends Authentication> toTest = authentication.getClass();
10        AuthenticationException lastException = null;
11        Authentication result = null;
12 
13        // 依次認證
14        for (AuthenticationProvider provider : getProviders()) {
15           if (!provider.supports(toTest)) {
16              continue;
17           }
18           try {
19              result = provider.authenticate(authentication);
20 
21              if (result != null) {
22                 copyDetails(authentication, result);
23                 break;
24              }
25           }
26           ...
27           catch (AuthenticationException e) {
28              lastException = e;
29           }
30        }
31        // 若是有Authentication信息,則直接返回
32        if (result != null) {
33             if (eraseCredentialsAfterAuthentication
34                     && (result instanceof CredentialsContainer)) {
35                    //移除密碼
36                 ((CredentialsContainer) result).eraseCredentials();
37             }
38              //發佈登陸成功事件
39             eventPublisher.publishAuthenticationSuccess(result);
40             return result;
41        }
42        ...
43        //執行到此,說明沒有認證成功,包裝異常信息
44        if (lastException == null) {
45           lastException = new ProviderNotFoundException(messages.getMessage(
46                 "ProviderManager.providerNotFound",
47                 new Object[] { toTest.getName() },
48                 "No AuthenticationProvider found for {0}"));
49        }
50        prepareException(lastException, authentication);
51        throw lastException;
52     }
53 }

 

到這裏,若是不糾結於AuthenticationProvider的實現細節以及安全相關的過濾器,認證相關的核心類其實都已經介紹完畢了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份認證器AuthenticationManager及其認證流程。姑且在這裏作一個分隔線。下面來介紹下AuthenticationProvider接口的具體實現。ProviderManager 中的List,會依照次序去認證,認證成功則當即返回,若認證失敗則返回null,下一個AuthenticationProvider會繼續嘗試認證,若是全部認證器都沒法認證成功,則ProviderManager 會拋出一個ProviderNotFoundException異常。

1.4 DaoAuthenticationProvider

AuthenticationProvider最最最經常使用的一個實現即是DaoAuthenticationProvider。顧名思義,Dao正是數據訪問層的縮寫,也暗示了這個身份認證器的實現思路。因爲本文是一個Overview,姑且只給出其UML類圖:

DaoAuthenticationProvider UMLDaoAuthenticationProvider UML

按照咱們最直觀的思路,怎麼去認證一個用戶呢?用戶前臺提交了用戶名和密碼,而數據庫中保存了用戶名和密碼,認證即是負責比對同一個用戶名,提交的密碼和保存的密碼是否相同即是了。在Spring Security中。提交的用戶名和密碼,被封裝成了UsernamePasswordAuthenticationToken,而根據用戶名加載用戶的任務則是交給了UserDetailsService,在DaoAuthenticationProvider中,對應的方法即是retrieveUser,雖然有兩個參數,可是retrieveUser只有第一個參數起主要做用,返回一個UserDetails。還須要完成UsernamePasswordAuthenticationToken和UserDetails密碼的比對,這即是交給additionalAuthenticationChecks方法完成的,若是這個void方法沒有拋異常,則認爲比對成功。比對密碼的過程,用到了PasswordEncoder和SaltSource,密碼加密和鹽的概念相信不用我贅述了,它們爲保障安全而設計,都是比較基礎的概念。

若是你已經被這些概念搞得暈頭轉向了,不妨這麼理解DaoAuthenticationProvider:它獲取用戶提交的用戶名和密碼,比對其正確性,若是正確,返回一個數據庫中的用戶信息(假設用戶信息被保存在數據庫中)。

1.5 UserDetails與UserDetailsService

上面不斷提到了UserDetails這個接口,它表明了最詳細的用戶信息,這個接口涵蓋了一些必要的用戶信息字段,具體的實現類對它進行了擴展。

 1 public interface UserDetails extends Serializable {
 2 
 3    Collection<? extends GrantedAuthority> getAuthorities();
 4 
 5    String getPassword();
 6 
 7    String getUsername();
 8 
 9    boolean isAccountNonExpired();
10 
11    boolean isAccountNonLocked();
12 
13    boolean isCredentialsNonExpired();
14 
15    boolean isEnabled();
16 }

 

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

UserDetailsService和AuthenticationProvider二者的職責經常被人們搞混,關於他們的問題在文檔的FAQ和issues中家常便飯。記住一點便可,敲黑板!!!UserDetailsService只負責從特定的地方(一般是數據庫)加載用戶信息,僅此而已,記住這一點,能夠避免走不少彎路。UserDetailsService常見的實現類有JdbcDaoImpl,InMemoryUserDetailsManager,前者從數據庫加載用戶,後者從內存中加載用戶,也能夠本身實現UserDetailsService,一般這更加靈活。它和Authentication接口很相似,好比它們都擁有username,authorities,區分他們也是本文的重點內容之一。Authentication的getCredentials()與UserDetails中的getPassword()須要被區分對待,前者是用戶提交的密碼憑證,後者是用戶正確的密碼,認證器其實就是對這二者的比對。Authentication中的getAuthorities()實際是由UserDetails的getAuthorities()傳遞而造成的。還記得Authentication接口中的getUserDetails()方法嗎?其中的UserDetails用戶詳細信息即是通過了AuthenticationProvider以後被填充的。

1.6 架構概覽圖

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

架構概覽圖架構概覽圖

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

相關文章
相關標籤/搜索