Spring Security - Acegi 框架介紹

概述 
    對於任何一個完整的應用系統,完善的認證和授權機制是必不可少的。Acegi Security(以下簡稱Acegi)是一個能爲基於Spring的企業應用提供強大而靈活安全訪問控制解決方案的框架,Acegi已經成爲Spring官方的一個子項目,所以也稱爲Spring Security。它通過在Spring容器中配置一組Bean,充分利用Spring的IoC和AOP功能,提供聲明式安全訪問控制的功能。雖然,現在Acegi也可以應用到非Spring的應用程序中,但在Spring中使用Acegi是最自然的方式。 
Acegi可以實現業務對象方法級的安全訪問控制粒度,它提供了以下三方面的應用程序的安全: 

? URL資源的訪問控制 
    如所有用戶(包括其名用戶)可以訪問index.jsp登錄頁面,而只有授權的用戶可以訪問/user/addUser.jsp頁面。Acegi允許通過正則表達式或Ant風格的路徑表達式定義URL模式,讓授權用戶訪問某一URL匹配模式下的對應URL資源。 

業務類方法的訪問控制 
    Spring容器中所有Bean的方法都可以被Acegi管理,如所有用戶可以調用BbtForum#getRefinedTopicCount()方法,而只有授權用戶可以調用BbtForum#addTopic()方法。 

? 領域對象的訪問控制 
    業務類方法代表一個具體的業務操作,比如更改、刪除、審批等,業務類方法訪問控制解決了用戶是否有調用某種操作的權限,但並未對操作的客體(領域對象)進行控制。對於我們的論壇應用來說,用戶可以調用BbtForum#updateUser(User user)方法更改用戶註冊信息,但應該僅限於更改自己的用戶信息,也即調用BbtForum#updateUser()所操作的User這個領域對象必須是受限的。 

    Acegi通過多個不同用途的Servlet過濾器對URL資源進行保護,在請求受保護的URL資源前,Acegi的Servlet過濾器判斷用戶是否有權訪問目標資源,授權者被開放訪問,而未未被授權者將被阻擋在大門之外。 
    Acegi通過Spring AOP對容器中Bean的受控方法進行攔截,當用戶的請求引發調用Bean的受控方法時,Acegi的方法攔截器開始工作,阻止未授權者的調用。 
    
    對領域對象的訪問控制建立在對Bean方法保護的基礎上,在最終開放目標Bean方法的執行前,Acegi將檢查用戶的ACL(Aeccess Control List:訪問控制列表)是否包含正要進行操作的領域對象,只有領域對象被授權時,用戶纔可以使用Bean方法對領域對象進行處理。此外,Acegi還可以對Bean方法返回的結果進行過濾,將一些不在當前用戶訪問權限範圍內的領域對象剔除掉——即傳統的數據可視域範圍的控制。一般來說,使用Acegi控制數據可視域未非理想的選擇,相反通過傳統的動態SQL的解決方案往往更加簡單易行。 

    從本質特性上來說,Servlet過濾器就是最原始的原生態AOP,所以我們可以說Acegi不但對業務類方法、領域對象訪問控制採用了AOP技術方案,對URL資源的訪問控制也使用了AOP的技術方案。使用AOP技術方案的框架是令人振奮的,這意味着,開發者可以在應用程序業務功能開發完畢後,輕鬆地通過Acegi給應用程序穿上安全保護的“鐵布衫”。

 

 Acegi體系結構 
    乘飛機前需要通過安檢,乘客必須提供身份證以驗證其身份。在通過安檢進入候機室後,國航、海航、南航等不同航空公司的飛機陸續到達,但你只能登上機票上對應航班的飛機。在登機後,只能坐在機票對應的座位上——你不能搶佔他人的座位,你不能在座位上刻字留念、你不能要求空姐打開機窗…… 

    乘飛機的過程最能體現安全控制的流程,我們可以從中找到身份認證、資源訪問控制、領域對象安全控制的對應物:安檢對應身份認證,登機對應資源訪問控制而按號就座則對應領域對象安全控制。 
    Acegi通過兩個組件對象完成以上安全問題的處理:AuthenticationManager(認證管理器)、AccessDecisionManager(訪問控制管理器),如圖 1所示:

圖 1 Acegi體系結構

 

    SecurityContextHolder是框架級的容器,它保存着和所有用戶關聯SecurityContext實例,SecurityContext承載着用戶(也稱認證主體)的身份信息的權限信息, AuthenticationManager、AccessDecisionManager將據此進行安全訪問控制。 
    
    SecurityContext的認證主體安全信息在一個HTTP請求線程的多個調用之間是共享的(通過ThreadLocal),但它不能在多個請求之間保持共享。爲了解決這個問題,Acegi將認證主體安全信息緩存於HttpSession中,當用戶請求一個受限的資源時,Acegi通過HttpSessionContextIntegrationFilter將認證主體信息從HttpSession中加載到SecurityContext實例中,認證主體關聯的SecurityContext實例保存在Acegi容器級的SecurityContextHolder裏。當請求結束之後,HttpSessionContextIntegrationFilter執行相反的操作,將SecurityContext中的認證主體安全信息重新轉存到HttpSession中,然後從SecurityContextHolder中清除對應的SecurityContext實例。通過HttpSession轉存機制,用戶的安全信息就可以在多個HTTP請求間共享,同時保證SecurityContextHolder中僅保存當前有用的用戶安全信息,其整體過程如圖 2所示:

圖 2 SecurityContext在HttpSession和請求線程間的轉交過程

 

    當用戶請求一個受限的資源時,AuthenticationManager首先開始工作,它象一個安檢入口,對用戶身份進行覈查,用戶必須提供身份認證的憑證(一般是用戶名/密碼)。在進行身份認證時,AuthenticationManager將身份認證的工作委託給多個AuthenticationProvider。因爲在具體的系統中,用戶身份可能存儲在不同的用戶信息安全系統中(如數據庫、CA中心、LDAP服務器),不同用戶信息安全系統需要不同的AuthenticationProvider執行諸如用戶信息查詢、用戶身份判斷、用戶授權信息獲取等工作。只要有一個AuthenticationProvider可以識別用戶的身份,AuthenticationManager就通過用戶身份認證,並將用戶的授權信息放入到SecurityContext中。 

   當用戶通過身份認證後,試圖訪問某個受限的程序資源時,AccessDecisionManager開始工作。AccessDecisionManager採用民主決策機制判斷用戶是否有權訪問目標程序資源,它包含了多個AccessDecisionVoter。在訪問決策時每個AccessDecisionVoter都擁有投票權,AccessDecisionManager統計投票結果,並按照某種決策方式根據這些投票結果決定最終是否向用戶開放受限資源的訪問

 

   重要組件類介紹 
    每個框架都有一些核心的概念,這些概念被固化爲類和接口,成爲框架的重要組件類。框架的管理類、操作類都在這些組件類的基礎上進行操作。在進入Acegi框架的具體學習前,有必要事先了解一下這些承載Acegi框架重要概念的組件類。 
    首先,我們要接觸是UserDetails接口,它代表一個應用系統的用戶,該接口定義了用戶安全相關的信息,如用戶名/密碼,用戶是否有效等信息,你可以根據以下接口方法進行相關信息的獲取: 
    String getUsername():獲取用戶名; 
     String getPassword():獲取密碼; 
     boolean isAccountNonExpired():用戶帳號是否過期; 
     boolean isAccountNonLocked():用戶帳號是否鎖定; 
     boolean isCredentialsNonExpired():用戶的憑證是否過期; 
     boolean isEnabled():用戶是否處於激活狀態。 
    當以上任何一個判斷用戶狀態的方法都返回false時,用戶憑證就被視爲無效。 
    UserDetails還定義了獲取用戶權限信息的方法:GrantedAuthority[] getAuthorities(),GrantedAuthority代表用戶權限信息,它定義了一個獲取權限描述信息(以字符串表示,如PRIV_COMMON)的方法:String getAuthority()。

圖 3 用戶和權限

 

    在未使用Acegi之前,我們可能通過類似User、Customer等領域對象表示用戶的概念,並在程序中編寫相應的用戶認證的邏輯。現在,你要做的一個調整是讓原先這些代表用戶概念的領域類實現UserDetails接口,這樣,Acegi就可以通過UserDetails接口訪問到用戶的信息了。 

    UserDetails可能從數據庫、LDAP等用戶信息資源中返回,這要求有一種機制來完成這項工作,UserDetailsService正是充當這一角色的接口。UserDetailsService接口很簡單,僅有一個方法:UserDetails loadUserByUsername(String username) ,這個方法通過用戶名獲取整個UserDetails對象。 
Authentication代表一個和應用程序交互的待認證用戶,Acegi從類似於登錄頁面、Cookie等處獲取待認證的用戶信息(一般是用戶名密碼)自動構造Authentication實例。

圖 4 Acegi的認證用戶

 

    Authentication可以通過Object getPrincipal()獲取一個代表用戶的對象,這個對象一般可以轉換爲UserDetails,從中可以取得用戶名/密碼等信息。在Authentication被AuthenticationManager認證之前,沒有任何權限的信息。在通過認證之後,Acegi通過UserDetails將用戶對應的權限信息加載到Authentication中。Authentication擁有一個GrantedAuthority[] getAuthorities()方法,通過該方法可以得到用戶對應的權限信息。 
    Authentication和UserDetails很容易被混淆,因爲兩者都有用戶名/密碼及權限的信息,接口方法也很類似。其實Authentication是Acegi進行安全訪問控制真正使用的用戶安全信息的對象,它擁有兩個狀態:未認證和已認證。UserDetails是代表一個從用戶安全信息源(數據庫、LDAP服務器、CA中心)返回的真正用戶,Acegi需要將未認證的Authentication和代表真實用戶的UserDetails進行匹配比較,通過匹配比較(簡單的情況下是用戶名/密碼是否一致)後,Acegi將UserDetails中的其它安全信息(如權限、ACL等)拷貝到Authentication中。這樣,Acegi安全控制組件在後續的安全訪問控制中只和Authentication進行交互。 

 

    由於Acegi對程序資源進行訪問安全控制時,一定要事先獲取和請求用戶對應的Authentication,Acegi框架必須爲Authentication提供一個“寓所”,以便在需要時直接從“寓所”把它請出來,作爲各種安全管理器決策的依據。 

    SecurityContextHolder就是Authentication容身的“寓所”,你可以通過SecurityContextHolder.getContext().getAuthenication()代碼獲取Authentication。細心觀察一下這句代碼,你會發現在SecurityContextHolder和Authentication之間存在一個getContext()中介,這個方法返回SecurityContext對象。SecurityContext這個半路殺出來的程咬金有什麼特殊的用途呢?我們知道Authentication是用戶安全相關的信息,請求線程其它信息(如登錄驗證碼等)則放置在SecurityContext中,構成了一個完整的安全信息上下文。SecurityContext接口提供了獲取和設置Authentication的方法: 
 Authentication getAuthentication() 
 void setAuthentication(Authentication authentication)

圖 5 認證用戶信息存儲器

 

    SecurityContextHolder是Acegi框架級的對象,它在內部通過ThreadLocal爲請求線程提供線程綁定的SecurityContext對象。這樣,任何參與當前請求線程的Acegi安全管理組件、業務服務對象等都可以直接通過SecurityContextHolder.getContext()獲取線程綁定的SecurityContext,避免通過方法入參的方式獲取用戶相關的SecurityContext。 

    線程綁定模式對於大多數應用來說是適合的,但是應用本身會創建其它的線程,那麼只有主線程可以獲得線程綁定SecurityContext,而主線程衍生出的新線程則無法得到線程綁定的SecurityContext。Acegi考慮到了這些不同應用情況,提供了三種綁定SecurityContext的模式: 
 SecurityContextHolder.MODE_THREADLOCAL:SecurityContext綁定到主線程,這是默認的模式;
 SecurityContextHolder.MODE_GLOBAL:SecurityContext綁定到JVM中,所有線程都使用同一個SecurityContext; 
 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL::SecurityContext綁定到主線程及由主線程衍生的線程中。 
    你可以通過SecurityContextHolder.setStrategyName(String strategyName)方法指定SecurityContext的綁定模式。

 

    用戶認證過程 
  Acegi支持多種方式的用戶認證:如典型的基於數據庫的認證、基於LDAP的認證、基於Yale中心認證等方式。不同的認證環境擁有不同的用戶認證方式,現在我們先拋開這些具體的細節,考察一下Acegi對受限資源進行訪問控制的典型過程: 
    1.你點擊一個鏈接訪問一個網頁; 
    2.瀏覽器發送一個請求到服務器,服務器判斷出你正在訪問一個受保護的資源; 
    3.如果此時你並未通過身份認證,服務器發回一個響應提示你進行認證——這個響應可能是一個HTTP響應代碼,抑或重定向到一個指定頁面; 
    4.根據系統使用認證機制的不同,瀏覽器或者重定向到一個登錄頁面中,或者由瀏覽器通過一些其它的方式獲取你的身份信息(如通過BASIC認證對話框、一個Cookie或一個X509證書); 
    5.瀏覽器再次將用戶身份信息發送到服務器上(可能是一個用戶登錄表單的HTTP POST信息、也可能是包含認證信息的HTTP報文頭); 
    6.服務器判斷用戶認證信息是否有效,如果無效,一般情況下,瀏覽器會要求你繼續嘗試,這意味着返回第3步。如果有效,則到達下一步; 
    7.服務器重新響應第2步所提交的原始請求,並判斷該請求所訪問的程序資源是否在你的權限範圍內,如果你有權訪問,請求將得到正確的執行並返回結果。否則,你將收到一個HTTP 403錯誤,這意味着你被禁止訪問。 
    在Acegi框架裏,你可以找到對應以上大多數步驟的類,其中ExceptionTranslationFilter、AuthenticationEntryPoint、AuthenticationProvider以及Acegi的認證機制是其中的代表者。 

    ExceptionTranslationFilter是一個Acegi的Servlet過濾器,它負責探測拋出的安全異常。當一個未認證用戶訪問服務器時,Acegi將引發一個Java異常。Java異常本身對HTTP請求以及如何認證用戶是一無所知的,ExceptionTranslationFilter適時登場,對這個異常進行處理,啓動用戶認證的步驟(第3步)。如果已認證用戶越權訪問一個資源,Acegi也將引發一個Java異常,ExceptionTranslationFilter則將這個異常轉換爲HTTP 403響應碼(第7步)。可見,Acegi通過異常進行通訊,
ExceptionTranslationFilter接收這些異常並作出相應的動作。 

    當ExceptionTranslationFilter通過Java異常發現用戶還未認證時,它到底會將請求重定向哪個頁面以要求用戶提供認證信息呢?這通過諮詢AuthenticationEntryPoint來達到目的——Acegi通過AuthenticationEntryPoint描述登錄頁面。 

    當你的瀏覽器通過HTTP表單或HTTP報文頭向服務器提供用戶認證信息時,Acegi需要將這些信息收集到Authentication中,Acegi用“認證機制”描述這一過程。此時,這個新生成Authentication只包含用戶提供的認證信息,但並未通過認證。
AuthenticationProvider負責對Authentication進行認證。AuthenticationProvider究竟如何完成這一過程呢?請回憶一下上節我們所介紹的UserDetails和UserDetailsService,大多數AuthenticationProvider通過UserDetailsService獲取和未認證的Authentication對應的UserDetails並進行匹配比較來完成這一任務。當用戶認證信息匹配時,Authentication被認爲是有效的,AuthenticationProvider進一步將UserDetails中權限、ACL等信息拷貝到Authentication。 
當Acegi通過認證機制收集到用戶認證信息並填充好Authentication後,Authentication將被保存到SecurityContextHolder中並處理用戶的原始請求(第7步)。 

    你完全可以拋開Acegi的安全機制,編寫自己的Servlet過濾器,使用自己的方案構建Authentication對象並將其放置到SecurityContextHolder中。也許你使用了CMA(Container 
Managed Authentication:容器管理認證),CMA允許你從ThreadLocal或JNDI中獲取用戶認證信息,這時你只要獲取這些信息並將其轉換爲Authentication就可以了。 

 

 

    安全對象訪問控制 
    Acegi稱受保護的應用資源爲“安全對象”,這包括URL資源和業務類方法。我們知道在Spring AOP中有前置增強、後置增強、異常增強和環繞增強,其中環繞增強的功能最爲強大——它不但可以在目標方法被訪問前攔截調用,還可以在調用返回前改變返回的結果,甚至拋出異常。Acegi使用環繞增強對安全對象進行保護。 
    Acegi通過AbstractSecurityInterceptor爲安全對象訪問提供一致的工作模型,它按照以下流程進行工作: 
    1. 從SecurityContext中取出已經認證過的Authentication(包括權限信息); 
    2. 通過反射機制,根據目標安全對象和“配置屬性”得到訪問目標安全對象所需的權限; 
    3. AccessDecisionManager根據Authentication的授權信息和目標安全對象所需權限做出是否有權訪問的判斷。如果無權訪問,Acegi將拋出AccessDeniedException異常,否則到下一步; 
4. 訪問安全對象並獲取結果(返回值或HTTP響應); 
5. AbstractSecurityInterceptor可以在結果返回前進行處理:更改結果或拋出異常。

 

    Acegi稱受保護的應用資源爲“安全對象”,這包括URL資源和業務類方法。我們知道在Spring AOP中有前置增強、後置增強、異常增強和環繞增強,其中環繞增強的功能最爲強大——它不但可以在目標方法被訪問前攔截調用,還可以在調用返回前改變返回的結果,甚至拋出異常。Acegi使用環繞增強對安全對象進行保護。     Acegi通過AbstractSecurityInterceptor爲安全對象訪問提供一致的工作模型,它按照以下流程進行工作:     1. 從SecurityContext中取出已經認證過的Authentication(包括權限信息);     2. 通過反射機制,根據目標安全對象和“配置屬性”得到訪問目標安全對象所需的權限;     3. AccessDecisionManager根據Authentication的授權信息和目標安全對象所需權限做出是否有權訪問的判斷。如果無權訪問,Acegi將拋出AccessDeniedException異常,否則到下一步; 4. 訪問安全對象並獲取結果(返回值或HTTP響應); 5. AbstractSecurityInterceptor可以在結果返回前進行處理:更改結果或拋出異常。

圖 6 AbstractSecurityInterceptor工作流程

 

    安全對象和一般對象的區別在於前者通過Acegi的“配置屬性”進行了描述,如“/view.jsp=PRIV_COMMON”配置屬性就將“/view.jsp”這個URL資源標識爲安全對象,它表示用戶在訪問/view.jsp時,必須擁有PRIV_COMMON這個權限。配置屬性通過XML配置文件,註解、數據庫等方式提供。安全對象通過配置屬性表示爲一個權限,這樣,Acegi就可以根據Authentication的權限信息獲知用戶可以訪問的哪些安全對象。 
    根據安全對象的性質以及具體實現技術,AbstractSecurityInterceptor擁有以下三個實現類: 
 FilterSecurityInterceptor:對URL資源的安全對象進行調用時,通過該攔截器實施環繞切面。該攔截器使用Servlet過濾器實現AOP切面,它本身就是一個Servlet過濾器; 
 MethodSecurityInterceptor:當調用業務類方法的安全對象時,可通過該攔截器類實施環繞切面; 
 AspectJSecurityInterceptor:和MethodSecurityInterceptor類似,它是針對業務類方法的攔截器,只不過它通過AspectJ實施AOP切面。

 

    Acegi版本升級的一些重大變化 
    Acegi項目開始於2003年,Acegi團隊在發佈新版本時非常謹慎,在本書寫作之時,Acegi最新版本爲1.0.3。在此之前Acegi已經發布了10多個預覽版本,由於Acegi框架優異的表現,許多大型應用早在Acegi 1.0正式版本發佈之前(2006年5月),就已經採用Acegi框架作爲其安全訪問控制的解決方案。

    在Acegi社區裏,來自世界各地衆多優秀的安全領域專家對Acegi的改進和發展獻計獻策,Acegi團隊廣泛聽取並吸收各種有益的建議,將它們融入到Acegi的框架中,使Acegi成爲構建在Spring基礎上企業應用的首選安全控制框架。 
Acegi 1.0.3版本相比於早期預覽版本發生了很大的變化,對於需要進行Acegi版本的項目來說,瞭解這一變化特別重要。下面,我們列出Acegi的一些重大的升級更新: 
 包名的更新:在0.9.0及之前的版本中,Acegi採用net.sf.acegisecurity包名前綴,在1.0.0版本之後更改爲org.acegisecurity(Hibernate也走過相同的道路,好在Acegi在正式版本發佈之時就完成了這種轉變); 

 ACL模塊的調整:ACL模塊發生了重大的調整,Acegi團隊接收了社區大量關於ACL模塊的反饋意見,重新設計了ACL模塊的底層結構,在性能、封裝性、靈活性上得到了質的提升。事實上,Acegi使用org.acegisecurity.acls包代替了原來的org.acegisecurity.acl包,後者將在後期的版本中刪除,由於這種傷筋動骨的變化,將很難兼容原來ACL模塊。不過,目前基於新框架的ACL模塊還沒有進行充分的測試,Acegi承諾在1.1.0版本發佈時提供最終的實現; 

 刪除了ContextHolder及其相關類:在Acegi 0.9版本中,ContextHolder及其相關類被徹底從Acegi項目中刪除。ContextHolder可以在多個HTTP請求中共享同一個ThreadLocal,這和Spring提倡的ThreadLocal只應在同一線程中共享相悖。現在,Acegi使用SecurityContextHolder替換ContextHolder,它的生命週期是一個HTTP 請求; 

 使用FilterChainProxy同時代理多個過濾器:在早期的版本中,Acegi通過FilterToBeanProxy將web.xml中的Servlet過濾器定義轉移到Spring容器中。這比直接在web.xml中配置Servlet過濾器要方便一些,但是Acegi框架往往需要定義多個Servlet過濾器,使web.xml配置文件變得冗長難看。在Acegi 0.8版本中提供FilterChainProxy,它可以同時代理多個Servlet過濾器並保證過濾器的順序。因此在新版本中,FilterChainProxy成爲推薦的選擇。 

    小結 
    Acegi是Spring項目下一個成熟的安全訪問控制框架,它允許利用了Spring IoC的AOP的功能完成安全對象的訪問控制。在Acegi框架中,SecurityContextHolder處於非常核心的位置,它是存放認證管理器用戶安全信息SecurityContext的“容器”,SecurityContext保存着用戶安全訪問控制所需的信息,直接被訪問決策管理器使用。HttpSessionContextIntegrationFilter通過在SecurityContextHolder和HttpSession中擺渡SecurityContext,使多個請求線程可以共享同一個SecurityContext。 

 

其它安全架構

    Acegi只是安全框架之一,其實還存在其它優秀的安全框架可供選擇

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