基於Acegi和Yale CAS實現單次登錄
你有多少個密碼?如果你和大多數人一樣,你很可能使用十餘個乃至更多的密碼來登錄日常訪問的各種系統。保管所有這些密碼是一個挑戰,而被迫登錄多個系統則令人厭煩。如果能夠只登錄一次就自動登錄了所有需要使用的系統,那該有多好。
單次登錄(Single Sign On,SSO)是一個熱門的安全話題。這個名字就已經表達了一切:一次登錄,訪問一切。耶魯大學技術和規劃組已經創建了一個名爲中心認證服務 (Central Authentication Service,CAS)的優秀SSO解決方案,它可以和Acegi共同工作。
關於設置和使用CAS的細節遠遠超出了本書的範圍。但我們將討論CAS採用的基本身份驗證方式,並探討如何與CAS一起使用Acegi。如果想知道關於CAS的更多信息,我們強烈推薦你訪問CAS的主頁http://tp.its.yale.edu/tiki/tiki-index.php?page= CentralAuthenticationService。
爲了理解Acegi在一個CAS身份驗證應用中的作用,重要的是先理解一個典型的CAS身份驗證場景是如何工作的。考慮一個請求受保護服務的流程,如圖11.5所示。
圖11.5 使用Yale CAS保護一個應用
當Web瀏覽器請求一個服務時①,服務通過在請求中尋找一個CAS票據來判斷用戶身份是否已通過驗證。如果未找到票據,則意味着該用戶尚未通過身份驗證。作爲結果,用戶被重定向到CAS的登錄頁面②。
在CAS的登錄頁面,用戶輸入他/她的用戶名和密碼。如果CAS成功認證了該用戶,則創建一個與請求的服務相關聯的票據。接着,CAS服務器將用戶重定向到用戶原先請求的服務(此時請求中已經有票據了)③。
服務再次在請求中尋找票據。這一次它找到了票據,並與CAS服務器聯繫以確認票據是有效的④。如果CAS的響應表明票據對當前請求的服務而言是有效的,服務就會允許用戶訪問應用。
以後,當用戶請求訪問另一個支持CAS的應用系統時,那個應用仍會與CAS聯繫。由於用戶之前已經認證,CAS會返回一個對該新應用有效的服務票據,而不會提示用戶再次登錄。
你應該理解的關於CAS的關鍵概念之一是:被保護的 應用從不處理用戶的憑證。當用戶被提示登錄應用系統時,他們實際上是登錄到了CAS服務器。應用系統自已從來沒有看到過用戶的憑證。應用系統使用的惟一安 全形式是通過詢問CAS服務器來驗證用戶的票據。這意味着只有一個應用(即CAS服務器)負責處理用戶認證,因此是非常好的解決方案。
當Acegi與CAS共同使用時,它承擔着幫助應用系統向CAS服務器驗證CAS票據的任務。這就使應用系統本身從CAS認證過程中解脫了出來。
Acegi通過使用CasAuthenticationProvider來完成這一任務。這是一個不關心用戶名和密碼的認證提供者,它只接受CAS票據作爲憑證。你可以在Spring配置文件中按如下方式配置一個CasAuthenticationProvider Bean:
<bean id="casAuthenticationProvider"
➥class="net.sf.acegisecurity.providers.cas.CasAuthenticationProvider">
<property name="ticketValidator">
<ref bean="ticketValidator"/>
</property>
<property name="casProxyDecider">
<ref bean="casProxyDecider"/>
</property>
<property name="statelessTicketCache">
<ref bean="statelessTicketCache"/>
</property>
<property name="casAuthoritiesPopulator">
<ref bean="casAuthoritiesPopulator"/>
</property>
<property name="key">
<value>some_unique_key</value>
</property>
</bean>
正如你所看到 的,CasAuthenticationProvider是通過和其他若干個Bean相互合作來完成它的工作的。這些Bean中的第一個是 ticketValidator Bean,它被裝配到ticketValidator屬性中。在Spring配置文件中它是這樣聲明的:
<bean id="ticketValidator" class="net.sf.acegisecurity.
➥providers.cas. ticketvalidator.CasProxyTicketValidator">
<property name="casValidate">
<value>https://localhost:8443/cas/proxyValidate</value>
</property>
<property name="serviceProperties">
<ref bean="serviceProperties"/>
</property>
</bean>
CasProxyTicketValidator通過聯繫CAS服務器來驗證CAS服務票據。屬性casValidate指定了CAS服務器處理驗證請求的URL。
配置中引用的serviceProperities Bean中包含了與CAS相關的Bean的重要配置信息:
<bean id="serviceProperties"
class="net.sf.acegisecurity.ui.cas.ServiceProperties">
<property name="service">
<value>https://localhost:8443/training/
➥j_acegi_cas_security_check</value>
</property>
</bean>
屬性service指定了一個URL,CAS在用戶成功登錄之後應該將用戶重定向至該URL。以後,在第11.4.3節中,你會看到該URL是如何被服務的。
回到 casAuthenticationProvider Bean,屬性casProxyDecider裝配了一個指向casProxyDecider Bean的引用,即一個到類型爲net.sf.acegisecurity.providers.cas. CasProxyDecider的Bean的引用。爲了理解casProxyDecider Bean的作用,你必須理解CAS如何支持代理服務。
CAS支持代理服務的概念,代理服務幫助另一個應用程序實現用戶的身份驗證。一個典型的代理服務的例子是門戶,門戶幫助由它所代表的portlet應用程序完成用戶身份驗證。當用戶登錄到一個門戶時,門戶通過代理票據也確保用戶隱含地登錄到它包含的應用系統中。
CAS如何處理代理票據是一個高級話題。我們建議你查閱CAS的文檔(http://tp.its.yale.edu/tiki/tiki-index.php?page=CasTwoOverview)獲取關於代理票據的更詳細的信息。在這裏,我們只需要指出CasProxyDecider負責決定是否接受代理票據。Acegi提供了CasProxyDecider的三個實現類:
n AcceptAnyCasProxy——接受來自任何服務的代理請求;
n NamedCasProxyDecider——接受來自一個已命名服務的列表的代理請求;
n RejectProxyTickets——拒絕任何代理請求。
爲簡單起見,讓我們假設你的應用系統不涉及代理服務。在這種情況下,RejectProxyTicket就成爲對casProxyDecider Bean來說最合適的CasProxyDecider:
<bean id="casProxyDecider"class="net.sf.acegisecurity.
➥ providers.cas.proxy.RejectProxyTickets"/>
屬性statelessTicketCache用於 支持無狀態的客戶端(比如遠程服務的客戶端),它們無法在HttpSession中存儲CAS票據。不幸的是,即使沒有無狀態客戶端要訪問你的應用系 統,statelessTicketCache屬性也是必不可少的。Acegi僅僅提供了一個實現,所以聲明一個 statelessTicketCache相當簡單:
<bean id="statelessTicketCache"class="net.sf.acegisecurity.
➥ providers.cas.cache.EhCacheBasedTicketCache">
<property name="minutesToIdle"><value>20</value></property>
</bean>
最後一個與CasAuthenticationProvider協同工作的Bean是casAuthoritiesPopulator Bean。作爲一個SSO實現,CAS只負責身份驗證——它不關心權限是如何分配給用戶的。爲了彌補這一差距,你需要一個net.sf.acegisecurity.providers.cas.CasAuthoritiesPopulator Bean。
Acegi只提供了一個 CasAuthoritiesPopulator的實現。DaoCasAuthoritiesPopulator使用一個認證DAO(如11.2.2節中 討論的)從數據庫中加載用戶明細信息。可以這樣聲明一個casAuthoritiesPopulator Bean:
<bean id="casAuthoritiesPopulator" class="net.sf.acegisecurity.
➥providers.cas.populator.DaoCasAuthoritiesPopulator">
<property name="authenticationDao">
<ref bean="inMemoryDaoImpl"/>
</property>
</bean>
最後,CasAuthenticationManager的key屬性指定了一個 String 值,認證管理器使用該字符串來識別之前已經認證的標誌。你可以把這個屬性設爲任意值。
關於使用CAS和Acegi實現SSO還有比 CasAuthenticationManager更多的內容。我們僅僅討論了CasAuthenticationProvider是如何進行身份驗證 的。在第11.4.3節中,你將看到當CasAuthenticationManager驗證用戶身份失敗時,是如何將用戶重定向到CAS登錄頁面的。
但就現在而言,讓我們先看一下Acegi是如何判斷一個己通過身份驗證的用戶是否擁有訪問受保護資源所需的恰當權限的。
控制訪問
身份驗證只是Acegi安全保護機制的第一步。一旦Acegi知道用戶的身份,它必須決定是否允許用戶訪問由它保護的資源。這就引出了訪問決策管理器。
正如認證管理器負責確定用戶的身份,訪問決策管理器負責決定用戶是否有恰當的權限訪問受保護的資源。一個訪問決策管理器是由net.sf.acegisecurity.AccessDecisionManager接口定義的:
public interface AccessDecisionManager {
public void decide(Authentication authentication, Object object,
ConfigAttributeDefinition config)
throws AccessDeniedException;
public boolean supports(ConfigAttribute attribute);
public boolean supports(Class clazz);
}
supports()方法根據受保護資源的類以及它 的配置屬性(受保護資源的訪問需求)判斷該訪問管理器是否能夠做出針對該資源的訪問決策。decide()方法是完成最終決策的地方。如果它沒有拋出 AccessDeniedException而返回,則允許訪問受保護的資源。否則,訪問被拒絕。
訪問決策投票
編寫一個你自己的AccessDecisionManager看上去是非常簡單的。但是,爲什麼做那些你本來用不着親自做的事?Acegi提供了適用於大多數情形的AccessDecisionManager的三個實現類:
n net.sf.acegisecurity.vote.AffirmativeBased
n net.sf.acegisecurity.vote.ConsensusBased
n net.sf.acegisecurity.vote.UnanimousBased
這三個訪問決策管理器的名字都相當奇怪,但當你考察過Acegi的授權策略之後就會明白它們的意思。
Acegi的訪問決策管理器負責最終決定一個通過身份驗證的用戶是否擁有訪問權限。然而,它們不是完全靠自己而完成這一決策的,而是通過徵詢一個或多個對某用戶是否有權訪問受保護資源進行投票的對象。一旦獲得所有的投票結果,決策管理器統計得票情況,並完成最終決策。
區分不同的訪問決策管理器的是它們如何計算出最終的決策。表11.2描述了每一個認證決策管理器是如何決定是否允許訪問的。
表11.2 Acegi的訪問決策管理器如何計票
訪問決策管理器 |
如 何 決 策 |
AffirmativeBased |
當至少有一個投票者投允許訪問票時允許訪問 |
ConsensusBased |
當所有投票者都投允許訪問票時允許訪問 |
UnanimousBased |
當沒有投票者投拒絕訪問票時允許訪問 |
在Spring配置文件中,所有的訪問決策管理器都是以相同的方式進行配置的。例如,以下一段XML摘要配置了一個UnanimousBased訪問決策管理器:
<bean id="accessDecisionManager"
class="net.sf.acegisecurity.vote.UnanimousBased">
<property name="decisionVoters">
<list>
<ref bean="roleVoter"/>
</list>
</property>
</bean>
你可以通過decisionVoters屬性爲訪問決策管理器提供一組投票者。在上述情況中,只有一個投票者,它引用了一個名爲roleVoter的Bean。讓我們看一下roleVoter是如何配置的。
決定如何投票
儘管訪問決策投票者對是否授權訪問某個受保護資源沒有最終發言權,它們在訪問決策過程中扮演了重要的角色。一個訪問決策投票者的工作是同時考慮用戶已擁有的授權和受保護資源的配置屬性中要求的授權。基於這一信息,訪問決策投票者通過投票爲訪問決策管理器做出決策提供支持。
一個訪問決策投票者是任何實現了net.sf.acegisecurity.vote.AccessDecisionVoter接口的對象:
public interface AccessDecisionVoter {
public static final int ACCESS_GRANTED = 1;
public static final int ACCESS_ABSTAIN = 0;
public static final int ACCESS_DENIED = -1;
public boolean supports(ConfigAttribute attribute);
public boolean supports(Class clazz);
public int vote(Authentication authentication, Object object,
ConfigAttributeDefinition config);
}
可以看到,AccessDecisionVoter 接口和AccessDecisionManager接口非常相似。最大的區別在於,AccessDecisionVoter沒有 AccessDecisionManager接口的返回類型爲void的decide()方法,而是有一個返回int的vote()方法。這是因爲訪問決 策投票者並不決定是否允許訪問,它僅僅就是否允許訪問投出它自己的一票。
在獲得投票機會時,訪問決策投票者能夠以下面三種方式進行投票:
n ACCESS_GRANTED——投票者希望允許訪問受保護的資源
n ACCESS_DENIED——投票者希望拒絕對受保護資源的訪問
n ACCESS_ABSTAIN——投票者不關心
與大多數Acegi的組件相同,你可以自由地編寫自 己的AccessDecisionVoter的實現類。然而,Acegi提供了一個很實用的投票者實現類RoleVoter,它當受保護資源的配置屬性代 表一個角色時進行投票。說得更具體一些,RoleVoter當受保護資源有一個名字由ROLE_開始的配置屬性時參與投票。
RoleVoter決定投票結果的方式是簡單地將受保護資源的所有配置屬性(以ROLE_作爲前綴)與認證用戶的所有授權進行比較。如果RoleVoter發現其中有一個是匹配的,則它投ACCESS_GRANTED票。否則,它將投ACCESS_DENIED票。
RoleVoter只在訪問所需的授權不是以ROLE_爲前綴時放棄投票。例如,如果受保護的資源僅僅需要一個非角色的授權(諸如CREATE_USER),則RoleVoter將放棄投票。
你可以在Spring配置文件通過以下方式配置一個RoleVoter:
<bean id="roleVoter"
class="net.sf.acegisecurity.vote.RoleVoter"/>
如前所述,RoleVoter只在受保護資源有以ROLE_爲前綴的配置屬性才進行投票。然而,ROLE_前綴只是默認值。你可以選擇通過設置rolePrefix屬性來重載這個默認前綴:
<bean id="roleVoter"
class="net.sf.acegisecurity.vote.RoleVoter">
<property name="rolePrefix">
<value>GROUP_</value>
</property>
</bean>
在這裏,默認的前綴被重載爲GROUP_。因此,這個RoleVoter現在將只針對以GROUP_爲前綴的權限進行授權投票。
11.3.2 決定如何投票
儘管訪問決策投票者對是否授權訪問某個受保護資源沒有最終發言權,它們在訪問決策過程中扮演了重要的角色。一個訪問決策投票者的工作是同時考慮用戶已擁有的授權和受保護資源的配置屬性中要求的授權。基於這一信息,訪問決策投票者通過投票爲訪問決策管理器做出決策提供支持。
一個訪問決策投票者是任何實現了net.sf.acegisecurity.vote.AccessDecisionVoter接口的對象:
public interface AccessDecisionVoter {
public static final int ACCESS_GRANTED = 1;
public static final int ACCESS_ABSTAIN = 0;
public static final int ACCESS_DENIED = -1;
public boolean supports(ConfigAttribute attribute);
public boolean supports(Class clazz);
public int vote(Authentication authentication, Object object,
ConfigAttributeDefinition config);
}
可以看到,AccessDecisionVoter 接口和AccessDecisionManager接口非常相似。最大的區別在於,AccessDecisionVoter沒有 AccessDecisionManager接口的返回類型爲void的decide()方法,而是有一個返回int的vote()方法。這是因爲訪問決 策投票者並不決定是否允許訪問,它僅僅就是否允許訪問投出它自己的一票。
在獲得投票機會時,訪問決策投票者能夠以下面三種方式進行投票:
n ACCESS_GRANTED——投票者希望允許訪問受保護的資源
n ACCESS_DENIED——投票者希望拒絕對受保護資源的訪問
n ACCESS_ABSTAIN——投票者不關心
與大多數Acegi的組件相同,你可以自由地編寫自 己的AccessDecisionVoter的實現類。然而,Acegi提供了一個很實用的投票者實現類RoleVoter,它當受保護資源的配置屬性代 表一個角色時進行投票。說得更具體一些,RoleVoter當受保護資源有一個名字由ROLE_開始的配置屬性時參與投票。
RoleVoter決定投票結果的方式是簡單地將受保護資源的所有配置屬性(以ROLE_作爲前綴)與認證用戶的所有授權進行比較。如果RoleVoter發現其中有一個是匹配的,則它投ACCESS_GRANTED票。否則,它將投ACCESS_DENIED票。
RoleVoter只在訪問所需的授權不是以ROLE_爲前綴時放棄投票。例如,如果受保護的資源僅僅需要一個非角色的授權(諸如CREATE_USER),則RoleVoter將放棄投票。
你可以在Spring配置文件通過以下方式配置一個RoleVoter:
<bean id="roleVoter"
class="net.sf.acegisecurity.vote.RoleVoter"/>
如前所述,RoleVoter只在受保護資源有以ROLE_爲前綴的配置屬性才進行投票。然而,ROLE_前綴只是默認值。你可以選擇通過設置rolePrefix屬性來重載這個默認前綴:
<bean id="roleVoter"
class="net.sf.acegisecurity.vote.RoleVoter">
<property name="rolePrefix">
<value>GROUP_</value>
</property>
</bean>
在這裏,默認的前綴被重載爲GROUP_。因此,這個RoleVoter現在將只針對以GROUP_爲前綴的權限進行授權投票。