Spring Security源碼閱讀1 - 核心組件和服務

Spring Security 是一個能夠爲基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在 Spring 應用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反轉),DI(Dependency Injection 依賴注入)和 AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減少了爲企業系統安全控制編寫大量重複代碼的工作。

Spring Security 擁有以下特性:

  • 對身份認證和授權的全面且可擴展的支持
  • 防禦會話固定、點擊劫持,跨站請求僞造等攻擊
  • 支持 Servlet API 集成
  • 支持與 Spring Web MVC 集成
  • 其他的特性

Spring Security與Spring和Spring Boot的關係如下:

在這裏插入圖片描述

目前Spring Security提供以下安全技術或支持與現有技術集成:

  • In-Memory認證
  • JDBC認證
  • LDAP認證
  • Active Directory認證
  • Remember-Me認證
  • OpenID
  • 匿名認證
  • JAAS(Java Authentication and Authorization) Provider
  • CAS認證
  • X.509認證
  • Basic And Digest認證
  • OAuth 2.0
  • SAML2

接下來先介紹Spring Security的核心組件開始。

1. 核心組件 - SecurityContextHolder, SecurityContext and Authentication

最基本的對象是SecurityContextHolder,它存儲當前應用程序安全上下文的詳細信息,其中包括當前使用應用程序的主體(通常是用戶)的詳細信息。如當前操作的用戶是誰,該用戶是否已經被認證,他擁有哪些角色權限等。默認情況下,SecurityContextHolder使用 ThreadLocal 來存儲這些詳細信息,這意味着 Security Context 始終可用於同一執行線程中的方法,即使 Security Context 未作爲這些方法的參數顯式傳遞。考慮到在用戶請求被處理後,Spring Security會自動清除線程,因此使用ThreadLocal 是線程安全的。

SecurityContextHolder支持三種安全策略:

  • SecurityContextHolder.MODE_THREADLOCAL: 每個線程有其自己的SecurityContextHolder
  • SecurityContextHolder.MODE_INHERITABLETHREADLOCAL: 繼承自安全線程的線程與安全線程有相同的安全標識
  • SecurityContextHolder.MODE_GLOBAL:所有線程共享相同的SecurityContextHolder

可能通過配置spring.security.strategy系統屬性來設置SecurityContextHolder的安全策略,但是大多數應用程序不需要修改SecurityContextHolder安全策略。

1.1 獲取當前用戶信息

SecurityContextHolder存儲了當前與應用程序交互的用戶信息,並且用戶信息與當前執行線程已綁定。 在Spring Security中使用Authentication類代表用戶信息,並且可以使用如下代碼塊在代碼的任意處獲得當前已驗證用戶的用戶名:

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

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

(1) getContext()獲得的是SecurityContext接口的實例,而該實例存儲在ThreadLocal中(分析見附錄-代碼1),代表當前線程需要的最少的安全信息。SecurityContext接口中定義了兩個方法,代碼如下:

public interface SecurityContext extends Serializable {
   Authentication getAuthentication();
   void setAuthentication(Authentication authentication);
}

並且通過跟蹤代碼可以確getPrincipal()返回的是UserDetails實例(分析見附錄-代碼1)。

1.2 Authentication

SecurityContext.getAuthentication()返回的Authentication也是一個接口,它的定義如下:

// org.springframework.security.core.Authentication.java
public interface Authentication extends Principal, Serializable {
    // 權限信息列表,默認是GrantedAuthority接口的一些實現類,通常是代表權限信息的一系列字符串
    Collection<? extends GrantedAuthority> getAuthorities();
    // 憑證信息以證明主體的正確性,如用戶在前端輸入的密碼
    Object getCredentials();
    // 其他信息,如IP地址,證書序列號等
    Object getDetails();
    // 主體的標識,如用戶名,大部分情況下返回的是UserDetails接口的實例
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication 直接繼承自 Principal 類,而Principal是位於 java.security 包中。通過Authentication 接口的實現類,我們可以得到用戶擁有的權限信息列表,密碼,用戶細節信息,用戶身份信息,認證信息等。

1.2.1 UserDetailService

Authentication代碼可知,可以通過其中的getPrincipal()方法獲得安全主體,雖然返回的是Object對象,但大多數情況下我們可以將其轉爲UserDetails對象UserDetails是Spring Security的核心類,代表一個安全主體並且是高度可擴展的,代碼如下:

public interface UserDetails extends Serializable {
   Collection<? extends GrantedAuthority> getAuthorities();
   String getPassword();
   String getUsername();
   boolean isAccountNonExpired();
   boolean isAccountNonLocked();
   boolean isCredentialsNonExpired();
   boolean isEnabled();
}

這裏需要注意的是UserDetailsgetPassword()AuthenticationgetCredentials()的不同:前者是用戶正確的密碼,後者是用戶提交的密碼憑證。

SecurityContextHolder中用UserDetails存儲安全主體信息,但是在應用程序中我們需要的安全主體信息可能更多(如需要email, empolyeeNumber等),此時我們可以通過繼承UserDetails接口實現自定義安全主體並存儲在SecurityContextHolder中,從而在使用時可以將SecurityContextHolder中獲得的UserDetails實例轉換爲自定義的實例。因此我們可以將UserDetails認爲是應用程序和Spring Security框架之間的適配器

爲了向SecurityContextHolder中提供自定義的UserDetails,只需要實向Spring容器中註冊一個實現了UserDetailsService接口的Bean即可,模板代碼如下:

@Service
publiic class AuthUserDetailsService implements UserDetailsService {
  @Override  
  (2)public UserDetails loadUserByUsername(userName: String) {
    	//你的邏輯
  }
}

(2)只需要在loadUserByUsername接口中添加定製的業務邏輯即可

Spring Security也提供了一些UserDetailsService的實現,如InMemoryDaoImpl)和JdbcDaoImpl但是不管如何提供UserDetailsService的實現,都可以通過SecurityContextHolder獲得UserDetailsService返回的數據

1.2.2 GrantedAuthority

除了主體,另一個Authentication提供的重要方法是getAuthorities()。這個方法提供了GrantedAuthority對象數組。GrantedAuthority是賦予到主體的權限,這些權限通常使用角色表示,比如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。這些角色會用於web驗證,方法驗證和領域對象驗證。GrantedAuthority對象通常使用UserDetailsService讀取,即在loadUserByUsername()方法中返回的UserDetails實例時設置。

1.3 總結

上面介紹的Spring Security中使用的核心組件及其功能如下:

  • SecurityContextHolder:提供幾種保存 SecurityContext的方式
  • SecurityContext:保存Authentication信息
  • Authentication:代表Spring Security中的主體
  • GrantedAuthority:主體的權限
  • UserDetails:代表主體信息
  • UserDetailsService:加載UserDetails

2. 核心服務 - AuthenticationManager, ProviderManager 和 AuthenticationProvider

AuthenticationManager接口是認證相關的核心接口,也是認證發起的出發點,在實際需求中,應用可能即允許用戶使用用戶名 + 密碼登錄,又允許用戶使用郵箱 + 密碼,手機號碼 + 密碼等形式登錄,所以要求認證系統要支持多種認證方式,因此需要一個接口定義認證的基本功能。AuthenticationManager的定義如下:

public interface AuthenticationManager {
   Authentication authenticate(Authentication authentication)
         throws AuthenticationException;
}

Spring Security 中 AuthenticationManager接口的默認實現是 ProviderManager, 其對Authentication authenticate(Authentication authentication)方法實現如下:

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
   Class<? extends Authentication> toTest = authentication.getClass();
   AuthenticationException lastException = null;
   AuthenticationException parentException = null;
   Authentication result = null;
   Authentication parentResult = null;
   boolean debug = logger.isDebugEnabled();

   (3)for (AuthenticationProvider provider : getProviders()) {
      if (!provider.supports(toTest)) {
         continue;
      }
     
      try {
         result = provider.authenticate(authentication);

         if (result != null) {
            (4)copyDetails(authentication, result);
            break;
         }
      }
      catch (AccountStatusException | InternalAuthenticationServiceException e) {
         prepareException(e, authentication);
         throw e;
      } catch (AuthenticationException e) {
         lastException = e;
      }
   }

   if (result == null && parent != null) {
      // Allow the parent to try.
      try {
         (5)result = parentResult = parent.authenticate(authentication);
      }
      catch (ProviderNotFoundException e) {
      }
      catch (AuthenticationException e) {
         lastException = parentException = e;
      }
   }

   if (result != null) {
      if (eraseCredentialsAfterAuthentication
            && (result instanceof CredentialsContainer)) {
         (6)((CredentialsContainer) result).eraseCredentials();
      }

      // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
      // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
      if (parentResult == null) {
         eventPublisher.publishAuthenticationSuccess(result);
      }
      return result;
   }

   if (lastException == null) {
      lastException = new ProviderNotFoundException(messages.getMessage(
            "ProviderManager.providerNotFound",
            new Object[] { toTest.getName() },
            "No AuthenticationProvider found for {0}"));
   }

   if (parentException == null) {
      prepareException(lastException, authentication);
   }

   (7)throw lastException;
}

(3) ProviderManager本身並不直接處理身份認證請求,而是將認證委託給AuthenticationProvider,依次查詢每個列表項是否可以執行身份認證。每個 Provider 要麼拋出異常要麼返回一個完全填充的 Authentication對象

(4) 認證成功後,會將原始的認證信息拷貝到Provider的返回結果中

(5) 若當前ProviderManager無法完成認證操作,且其包含父級認證器,則轉交給父級認證器嘗試進行認證

(6) 完成認證,從authentication對象中刪除私密數據,防止一些機密數據(如用戶密碼)過長時間保留在內存中

(7) 如果認證失敗,則拋出AuthenticationException

Spring Security提供了很多認證Provider,如:

  • DaoAuthenticationProvider
  • AnonymousAuthenticationProvider
  • RememberMeAuthenticationProvider

所有的Provider都繼承自AuthenticationProvider接口,代碼如下:

public interface AuthenticationProvider {
   // 驗證請求
   Authentication authenticate(Authentication authentication)
         throws AuthenticationException;

   // 判斷是否支持對authentication的認證
   boolean supports(Class<?> authentication);
}

比如在DaoAuthenticationProvider中使用UserDetailsService根據用戶名獲得UserDetails,再通過比對用戶密碼判斷用戶是否合法。

但是需要注意的是,在使用相應的認證機制時,必須爲其提供相應的認證Provider,否則會導致認證失敗。如JA-SIG CAS認證,其必須使用CasAuthenticationProvider

3. 總結

  • 應用中可以通過SecurityContextHolder獲得認證信息Authentication
  • 應用可以從獲得的Authentication實例中獲得認證後的用戶信息,如用戶名和權限
  • 應用通過繼承UserDetails接口定製合適的UserDetails(如新增getEmail()函數)
  • 應用通過繼承UserDetailsService接口將系統中存儲的用戶信息轉換爲Spring Security的UserDetails實例
  • Spring Security中ProviderManager爲認證的入口
  • ProviderManager通過AuthenticationProvider完成認證

參考

  1. Spring Security架構簡介
  2. Spring Security 5.2官方文檔

附錄

代碼1

SecurityContextHolder.getContext()代碼如下:

public static SecurityContext getContext() {1)return strategy.getContext();
}

(1) 默認情況下strategy的指向的是ThreadLocalSecurityContextHolderStrategy實例,代碼如下:

private static void initialize() {
   if (!StringUtils.hasText(strategyName)) {
      // Set default
      strategyName = MODE_THREADLOCAL;
   }

   if (strategyName.equals(MODE_THREADLOCAL)) {
      (2) strategy = new ThreadLocalSecurityContextHolderStrategy();
   }
   ......
}

(2) 可以確定最終調用的是ThreadLocalSecurityContextHolderStrategy實例中的getContexxt()方法,代碼如下:

public SecurityContext getContext() {
   (3)SecurityContext ctx = contextHolder.get();

   if (ctx == null) {
      ctx = createEmptyContext();
      contextHolder.set(ctx);
   }

   return ctx;
}

(3) contextHolder的定義如下:

private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

因此默認獲得的SecurityContext實例是存放在ThreadLocal中的。

Github博客地址
知乎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章