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 認證環節
一個基本的認證環節包括:
- 使用者輸入使用者名稱和密碼
- 系統驗證使用者名稱密碼正確
- 系統獲取該使用者的角色、許可權等資訊。
以上三個步驟完成了一個認證過程,在Spring Security中,相應地完成了以下動作:
- 後端獲取到使用者名稱和密碼並用之生成一個UsernamePasswordAuthenticationToken物件,UsernamePasswordAuthenticationToken是Authentication的一個實現類。
- token被傳入AuthenticationManager的例項中進行校驗
- 認證成功後,AuthenticationManager會返回一個Authentication例項,其中包括了使用者所有細節資訊,包括角色、許可權等
- 通過呼叫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在處理安全物件的請求時候,流程如下:
- 檢視和當前請求關聯的配置屬性(configuration attributes)
- 將當前的安全物件、Authentication物件以及配置屬性提交給AccessDecisionManager,由其做一個授權的決定。
- 在呼叫發生的位置可選的更換Authentication物件
- 完成授權後,允許安全物件的呼叫進行。
- 當呼叫返回後,即刻呼叫AfterInvocationManager(如果配置了)
Configuration Attributes
配置屬性用介面ConfigAttribute表示,可以理解為被AbstractSecurityInterceptor使用的具有特殊意義的字串。AbsractSecurityInterceptor中配置了SecurityMetadataSource用來檢視安全物件的配置屬性。配置屬性可以用來簡單表示一個角色名,或者更復雜的意義,它的用處取決於AccessDecisionManager實現的複雜性。或者簡單來說,配置屬性只是表示特殊含義的,比如角色名的字串,但是其具體如何解讀,取決於AccessDedcisionManager的實現。舉個簡單得例子,當我們使用預設的AccessDedcisionManager的實現時,可以在一個方法或者一個url請求的註釋里加入配置屬性ROLE_A,這表示,只有當使用者的GrantedAuthority匹配ROLE_A的時候,才被允許使用這個方法或呼叫這個請求。這裡只做簡單得說明,具體使用在後文中展開。
Security interceptors and the "secure object" model
下圖是安全攔截器和安全物件的模型,給出了各個元件之間的關係,有個別元件在簡介中沒有提到,將在後文的使用說明中展開。