Spring Security系列之體系結構概述

核心組件

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

SecurityContextHolder

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

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

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這個容器。

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. <1> Authentication是spring security包中的接口,直接繼承自Principal類,而Principal是位於java.security包中的。可以見得,Authentication在spring security中是最高級別的身份/認證的抽象。
  2. <2> 由這個頂級接口,我們可以得到用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息。

還記得SecurityContextHolder節中,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這個實現類。
  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");
}
}

/<grantedauthority>/<grantedauthority>

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

AuthenticationManager

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

roviderManager 內部會維護一個 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;
}
}

/<authenticationprovider>

ProviderManager 中的List<AuthenticationProvider>,會依照次序去認證,認證成功則立即返回,若認證失敗則返回null,下一個AuthenticationProvider會繼續嘗試認證,如果所有認證器都無法認證成功,則ProviderManager會拋出一個ProviderNotFoundException異常。

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

DaoAuthenticationProvider

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

Spring Security系列之體系結構概述

按照我們最直觀的思路,怎麼去認證一個用戶呢?用戶前臺提交了用戶名和密碼,而數據庫中保存了用戶名和密碼,認證便是負責比對同一個用戶名,提交的密碼和保存的密碼是否相同便是了。在Spring Security中。提交的用戶名和密碼,被封裝成了UsernamePasswordAuthenticationToken,而根據用戶名加載用戶的任務則是交給了UserDetailsService,在DaoAuthenticationProvider中,對應的方法便是retrieveUser,雖然有兩個參數,但是retrieveUser只有第一個參數起主要作用,返回一個UserDetails。還需要完成UsernamePasswordAuthenticationToken和UserDetails密碼的比對,這便是交給additionalAuthenticationChecks方法完成的,如果這個void方法沒有拋異常,則認為比對成功。比對密碼的過程,用到了PasswordEncoder和SaltSource,密碼加密和鹽的概念相信不用我贅述了,它們為保障安全而設計,都是比較基礎的概念。如果你已經被這些概念搞得暈頭轉向了,不妨這麼理解DaoAuthenticationProvider:它獲取用戶提交的用戶名和密碼,比對其正確性,如果正確,返回一個數據庫中的用戶信息(假設用戶信息被保存在數據庫中)。

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,通常這更加靈活。

架構概覽圖

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

Spring Security系列之體系結構概述

如果對Spring Security的這些概念感到理解不能,不用擔心,因為這是Architecture First導致的必然結果,先過個眼熟。關注、轉發、評論頭條號每天分享java 知識,私信回覆“555”贈送一些Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式資料


分享到:


相關文章: