小米安全Dayeh:《Spring Security入坑指南1》

小米安全中心發表於2019-03-15

Spring Security 01 — Basic Introduction Part I
Spring Security 5.1.4 RELEASE

 

對於使用Java進行開發的同學來說,Spring算是一個比較熱門的選擇,包括現在流行的Spring Boot更是能快速上手,並讓開發者更好的關注業務核心的開發,而免去了原來冗雜的配置過程。從Spring 4開始推薦使用程式碼進行配置,更是降低了配置的難度,遠離了讓人看得頭大的xml配置檔案。

 

這篇文章主要想系統的介紹一下Spring Security這個框架。當需要進行一些認證授權的開發時,常用的Java安全框架主要有Apache Shiro和Spring Security。兩者相比,Shiro更為輕量化,簡單易用,而Spring Security作為Spring的親兒子,功能更強大,和Spring專案的結合度更好,而使用的學習成本相較於Shiro會略高一些。

 

在講程式碼之前,想先介紹一下Spring Security中的一些基本元件及服務,以便於更好理解後文的程式碼。基本架構的介紹,主要來自於官方文件,進行了選擇性的翻譯,參考的版本是Spring Security 5.1.4 RELEASE,感興趣的同學也可以直接前往閱讀英文原文。

 

1 基本架構

 

1.1 核心元件

 

從Spring Security 3.0開始,元件spring-security-corejar包中的內容進行了精簡,不再包含任何web應用安全,LDAP或名稱空間配置的程式碼。

 

SecurityContextHolder

 

最基本的物件,用來儲存當前的安全上下文(security context),包含了當前登入的使用者資訊。預設使用ThreadLocal儲存細節資訊,因此這些資訊對於同一個執行緒內容呼叫的方法都是可用的。當使用者的請求處理完成後,框架會自動清理執行緒而不必使用者關心。但是由於某些應用由於其對執行緒的使用方式,並不適合使用ThreadLocal,則需要根據情況,在啟動前設定SecurityContextHolder的策略。

 

在SecurityContextHolder中儲存了當前活躍使用者的資訊。Spring Security使用一個Authentication物件來表示這些資訊。在程式的任何地方,都可以用以下程式碼來獲取當前認證使用者的資訊。

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

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

UserDetailsService

 

在介面UserDetailService中只有一個方法,接收一個字串返回一個UserDetails物件,當認證成功後,UserDetails會被用來構造一個Authentication物件儲存在SecurityContextHolder中。

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

UserDetailsService的實現,主要是用來載入使用者資訊,並向其他元件提供這些資訊,並不能進行認證使用者的操作,認證的操作由AuthenticationManager完成。

 

如果使用者想自定義一個認證流程,則需要實現AuthenticationProvider介面。

 

GrantedAuthority

 

Authentication的getAuthorities( )方法返回GrantedAuthority的物件陣列。GrantAuthority物件一般由UserDetailsService載入。

 

總結

 

ScurityContextHolder獲取SecurityContext

 

SecurityContext儲存了Authentication以及其他一些請求相關的安全資訊

 

Authentication表示一個認證使用者資訊

 

GrantedAuthority表示授予使用者的許可權資訊

 

UserDetails包含了構建Authentication物件需要的必要資訊,這些資訊來自於應用的DAO或其他資料來源

 

UserDetailsService根據傳入使用者名稱字串構建一個UserDetails物件

 

1.2 認證環節

 

一個基本的認證環節包括:

  1. 使用者輸入使用者名稱和密碼
  2. 系統驗證使用者名稱密碼正確
  3. 系統獲取該使用者的角色、許可權等資訊。

以上三個步驟完成了一個認證過程,在Spring Security中,相應地完成了以下動作:

  1. 後端獲取到使用者名稱和密碼並用之生成一個UsernamePasswordAuthenticationToken物件,UsernamePasswordAuthenticationToken是Authentication的一個實現類。
  2. token被傳入AuthenticationManager的例項中進行校驗
  3. 認證成功後,AuthenticationManager會返回一個Authentication例項,其中包括了使用者所有細節資訊,包括角色、許可權等
  4. 通過呼叫SecurityContextHolder.getContext().setAuthentication(…)建立安全上下文,傳入Authentication物件

完成以上過程後,當前使用者認證完成。以下程式碼示範了一個認證環節最基本的流程(並非SpringSecurity框架原始碼)

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

public class AuthenticationExample {

// 0. 建立一個AuthenticationManager例項,之後用於使用者校驗 (具體實現在下方)  
private static AuthenticationManager am = new SampleAuthenticationManager();

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

    // 1. 使用者在介面輸入使用者名稱密碼
    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 {
        // 2. 使用者名稱和密碼生成一個UsernamePasswordAuthenticationToken物件
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);

        // 3. 使用AuthenticationManager例項校驗token
        Authentication result = am.authenticate(request);

        // 4. 校驗成功,將包含使用者資訊的Authentication例項加入security context
        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());
}
}


// AuthenticationManager的實現
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 {

    //該方法內寫認證通過的條件,此處demo判斷條件是,使用者名稱等於密碼即認證通過
    if (auth.getName().equals(auth.getCredentials())) {
    return new UsernamePasswordAuthenticationToken(auth.getName(),
        auth.getCredentials(), AUTHORITIES);
    }
    throw new BadCredentialsException("Bad Credentials");
}
}

直接設定SecurityContextHolder

 

Spring Security並不關心一個Authentication例項是如何放進SecurityContextHolder的,只要保證在SecurityContextHolder中有一個有效的Authentication表示一個認證的使用者即可,然後AbstractSecurityInterceptor便可用來授權使用者的操作了。因此,開發者亦可選擇使用其他認證框架提供的認證資訊。開發者只需要寫使用一個過濾器獲取來自第三方的使用者資訊,然後構造一個Authentication物件放入到SecurityContextHolder即可。但是如果不使用內建的認證,有些原本自動完成的事情就需要由開發者來處理了,比如需要在一開始建立HTTP session來快取上下文。

 

1.3 Web應用中的認證

 

在處理Web應用中的認證,Spring Security中主要參與的有ExceptionTranslationFilter和AuthenticationEntryPoint,以及一個認證機制,用來呼叫AuthenticationManager完成核心的認證部分。

 

ExceptionTranslationFilter

 

是一個Spring Security的過濾器,用來檢測所有丟擲的Spring Security的異常,通常這些異常都由AbstractSecurityInterceptor丟擲。AbstractSecurityInterceptor只負責丟擲異常,而ExceptionTranslationFilter則負責確定如何處理異常,比如當前使用者認證了但許可權不夠,則返回403錯誤碼,或者當前使用者還未認證,則發起一個AuthenticationPoint。

 

Authentication Mechanism

 

當瀏覽器提交使用者的認證資訊後,伺服器需要收集這些資訊,而在Spring Security中,從客戶端獲取認證資訊的功能稱為“認證機制” (authentication mechanism)。比如Basic authentication,當收集到客戶端提交的認證資訊後,後端就會建立一個Authentication物件,然後交給AuthenticationManager校驗。

 

隨後authentication mechanism會收到一個包含完整資訊的Authentication物件,並認為請求合法,然後把Authentication放入SecurityContextHolder,隨後原請求便會發起重試。如果AuthenticationManager拒絕了請求,那麼認證機制便會要求客戶端重試。

 

儲存使用者認證資訊

 

一般在一個Web應用中,使用者登入後,伺服器會快取使用者的認證資訊,使用者後續的操作通過其session id進行身份認證。Spring Security框架中,儲存SecurityContext的任務交給SecurityContextPersistenceFilter,其預設將安全上下文資訊儲存為HttpSessio屬性。每當請求來,它都會通過SecurityContextHolder來獲取認證資訊,並在請求結束後,清除SecurityContextHolder。出於安全考慮,不要直接從HttpSession中去獲取安全上下文,而應該通過SecurityContextHolder獲取。

 

1.4 許可權控制(授權)

 

Spring Security的許可權控制,依賴於AOP。許可權控制可以應用在方法呼叫上,也可用在web請求上。Spring Security中主要負責進行許可權控制決定的是AccessDecisionManager。

 

Secure Objects

 

安全物件指一切可以加上安全配置的物件,最常見的例子是方法的呼叫和web請求。

 

每個支援的安全物件都有一個自己的攔截器,這個攔截器是AbstractSecurityInterceptor子類,當AbstractSecurityInterceptor被呼叫的時候,SecurityContextHolder中會包含一個有效的Authentication物件,如果當前使用者主體已經被認證。AbstractSecurityInterceptor在處理安全物件的請求時候,流程如下:

  1. 檢視和當前請求關聯的配置屬性(configuration attributes)
  2. 將當前的安全物件、Authentication物件以及配置屬性提交給AccessDecisionManager,由其做一個授權的決定。
  3. 在呼叫發生的位置可選的更換Authentication物件
  4. 完成授權後,允許安全物件的呼叫進行。
  5. 當呼叫返回後,即刻呼叫AfterInvocationManager(如果配置了)

Configuration Attributes

 

配置屬性用介面ConfigAttribute表示,可以理解為被AbstractSecurityInterceptor使用的具有特殊意義的字串。AbsractSecurityInterceptor中配置了SecurityMetadataSource用來檢視安全物件的配置屬性。配置屬性可以用來簡單表示一個角色名,或者更復雜的意義,它的用處取決於AccessDecisionManager實現的複雜性。或者簡單來說,配置屬性只是表示特殊含義的,比如角色名的字串,但是其具體如何解讀,取決於AccessDedcisionManager的實現。舉個簡單得例子,當我們使用預設的AccessDedcisionManager的實現時,可以在一個方法或者一個url請求的註釋里加入配置屬性ROLE_A,這表示,只有當使用者的GrantedAuthority匹配ROLE_A的時候,才被允許使用這個方法或呼叫這個請求。這裡只做簡單得說明,具體使用在後文中展開。

 

Security interceptors and the "secure object" model

 

下圖是安全攔截器和安全物件的模型,給出了各個元件之間的關係,有個別元件在簡介中沒有提到,將在後文的使用說明中展開。

 

相關文章