CAS 認證
概述
JA-SIG 開發了一個企業級的單點登入系統,名為 CAS。與其他方案不同,JA-SIG 的中央認證服務是開源的、廣泛使用的、易於理解的、平臺獨立的,並且支援代理功能。Spring Security 完全支援 CAS,併為從 Spring Security 的單應用部署到由企業級 CAS 伺服器保護的多應用部署提供了簡單的遷移路徑。
您可以在 www.apereo.org 瞭解更多關於 CAS 的資訊。您還需要訪問此網站下載 CAS 伺服器檔案。
CAS 工作原理
雖然 CAS 網站上詳細介紹了 CAS 的架構,但我們在此處 Spring Security 的上下文中再次提供一般概述。Spring Security 3.x 支援 CAS 3。在撰寫本文時,CAS 伺服器版本為 3.4。
在您的企業中,您需要設定一個 CAS 伺服器。CAS 伺服器只是一個標準的 WAR 檔案,因此設定您的伺服器並不困難。在 WAR 檔案內部,您將自定義顯示給使用者的登入和其他單點登入頁面。
部署 CAS 3.4 伺服器時,您還需要在 CAS 附帶的 deployerConfigContext.xml 中指定一個 AuthenticationHandler。AuthenticationHandler 有一個簡單的方法,它返回一個布林值,指示給定的一組憑據是否有效。您的 AuthenticationHandler 實現需要連結到某種後端認證儲存庫,例如 LDAP 伺服器或資料庫。CAS 本身開箱即用提供了許多 AuthenticationHandler 來協助完成此任務。當您下載並部署伺服器 WAR 檔案時,它已設定為成功驗證輸入與其使用者名稱匹配的密碼的使用者,這對於測試很有用。
除了 CAS 伺服器本身,其他關鍵參與者當然是部署在您企業中的安全 Web 應用程式。這些 Web 應用程式被稱為“服務”。服務有三種類型。驗證服務票據的服務,可以獲取代理票據的服務,以及驗證代理票據的服務。驗證代理票據有所不同,因為必須驗證代理列表,而且代理票據通常可以重複使用。
Spring Security 和 CAS 互動序列
Web 瀏覽器、CAS 伺服器和 Spring Security 保護的服務之間的基本互動如下
-
Web 使用者正在瀏覽服務的公共頁面。CAS 或 Spring Security 不涉及其中。
-
使用者最終請求一個安全頁面或其使用的某個 bean 是安全的。Spring Security 的
ExceptionTranslationFilter將檢測到AccessDeniedException或AuthenticationException。 -
由於使用者的
Authentication物件(或缺乏此物件)導致了AuthenticationException,ExceptionTranslationFilter將呼叫配置的AuthenticationEntryPoint。如果使用 CAS,這將是CasAuthenticationEntryPoint類。 -
CasAuthenticationEntryPoint將把使用者的瀏覽器重定向到 CAS 伺服器。它還將指示一個service引數,這是 Spring Security 服務(您的應用程式)的回撥 URL。例如,瀏覽器被重定向到的 URL 可能是 my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas。 -
使用者瀏覽器重定向到 CAS 後,系統會提示他們輸入使用者名稱和密碼。如果使用者提供了一個會話 Cookie,表明他們之前已登入,則不會再次提示他們登入(此過程有一個例外,我們稍後會討論)。CAS 將使用上面討論的
PasswordHandler(如果使用 CAS 3.0,則為AuthenticationHandler)來決定使用者名稱和密碼是否有效。 -
成功登入後,CAS 會將使用者的瀏覽器重定向回原始服務。它還會包含一個
ticket引數,這是一個代表“服務票據”的不透明字串。繼續我們之前的示例,瀏覽器重定向到的 URL 可能是 server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ。 -
回到服務 Web 應用程式中,
CasAuthenticationFilter始終偵聽對/login/cas的請求(這是可配置的,但在本介紹中我們將使用預設值)。處理過濾器將構造一個表示服務票據的UsernamePasswordAuthenticationToken。主體將等於CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而憑據將是服務票據的不透明值。然後,此身份驗證請求將交給配置的AuthenticationManager。 -
AuthenticationManager實現將是ProviderManager,它又配置了CasAuthenticationProvider。CasAuthenticationProvider僅響應包含 CAS 特定主體(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)的UsernamePasswordAuthenticationToken和CasAuthenticationToken(稍後討論)。 -
CasAuthenticationProvider將使用TicketValidator實現來驗證服務票據。這通常是Cas20ServiceTicketValidator,它是 CAS 客戶端庫中包含的類之一。如果應用程式需要驗證代理票據,則使用Cas20ProxyTicketValidator。TicketValidator向 CAS 伺服器發出 HTTPS 請求以驗證服務票據。它可能還包含一個代理回撥 URL,此示例中包含了該 URL:my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor。 -
回到 CAS 伺服器端,驗證請求將被接收。如果提交的服務票據與票據發往的服務 URL 匹配,CAS 將以 XML 形式提供肯定響應,指示使用者名稱。如果身份驗證中涉及任何代理(如下所述),代理列表也包含在 XML 響應中。
-
[可選] 如果對 CAS 驗證服務的請求中包含代理回撥 URL(在
pgtUrl引數中),CAS 將在 XML 響應中包含一個pgtIou字串。此pgtIou代表一個代理授予票據借據。CAS 伺服器將建立自己的 HTTPS 連線返回到pgtUrl。這是為了相互驗證 CAS 伺服器和宣告的服務 URL。HTTPS 連線將用於將代理授予票據傳送到原始 Web 應用程式。例如,server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH。 -
Cas20TicketValidator將解析從 CAS 伺服器收到的 XML。它將向CasAuthenticationProvider返回一個TicketResponse,其中包括使用者名稱(必需)、代理列表(如果涉及任何代理)和代理授予票據借據(如果請求了代理回撥)。 -
接下來,
CasAuthenticationProvider將呼叫一個已配置的CasProxyDecider。CasProxyDecider指示TicketResponse中的代理列表是否可被服務接受。Spring Security 提供了幾種實現:RejectProxyTickets、AcceptAnyCasProxy和NamedCasProxyDecider。這些名稱大部分都具有自解釋性,除了NamedCasProxyDecider,它允許提供一個受信任代理的List。 -
CasAuthenticationProvider接下來將請求AuthenticationUserDetailsService載入適用於Assertion中包含的使用者的GrantedAuthority物件。 -
如果沒有問題,
CasAuthenticationProvider會構造一個CasAuthenticationToken,其中包含TicketResponse中包含的詳細資訊和一組至少包含FACTOR_BEARER的GrantedAuthority。 -
控制權隨後返回給
CasAuthenticationFilter,後者將建立的CasAuthenticationToken放置在安全上下文中。 -
使用者的瀏覽器被重定向到導致
AuthenticationException的原始頁面(或根據配置自定義目標)。
很高興你還在!現在讓我們看看如何配置它
CAS 客戶端配置
由於 Spring Security,CAS 的 Web 應用程式端變得很容易。假設您已經瞭解使用 Spring Security 的基礎知識,因此此處不再贅述。我們將假設正在使用基於名稱空間的配置,並根據需要新增 CAS bean。每個部分都建立在前一個部分的基礎上。Spring Security 示例中可以找到完整的 CAS 示例應用程式。
服務票據認證
本節描述如何設定 Spring Security 以驗證服務票據。通常,Web 應用程式只需要這樣做。您需要在應用程式上下文中新增一個 ServiceProperties bean。這表示您的 CAS 服務
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
<property name="service"
value="https://:8443/cas-sample/login/cas"/>
<property name="sendRenew" value="false"/>
</bean>
service 必須等於一個由 CasAuthenticationFilter 監控的 URL。sendRenew 預設為 false,但如果您的應用程式特別敏感,則應將其設定為 true。此引數的作用是告知 CAS 登入服務單點登入不可接受。相反,使用者需要重新輸入其使用者名稱和密碼才能獲得對服務的訪問許可權。
應配置以下 bean 以啟動 CAS 身份驗證過程(假設您正在使用名稱空間配置)
<security:http entry-point-ref="casEntryPoint">
...
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
</security:http>
<bean id="casFilter"
class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>
<bean id="casEntryPoint"
class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<property name="loginUrl" value="https://:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>
為了使 CAS 正常執行,ExceptionTranslationFilter 的 authenticationEntryPoint 屬性必須設定為 CasAuthenticationEntryPoint bean。這可以透過使用 entry-point-ref 輕鬆完成,如上例所示。CasAuthenticationEntryPoint 必須引用 ServiceProperties bean(如上所述),該 bean 提供企業 CAS 登入伺服器的 URL。使用者的瀏覽器將重定向到此處。
CasAuthenticationFilter 與 UsernamePasswordAuthenticationFilter(用於基於表單的登入)具有非常相似的屬性。您可以使用這些屬性來自定義諸如認證成功和失敗的行為。
接下來你需要新增一個 CasAuthenticationProvider 及其協作者
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="casAuthenticationProvider" />
</security:authentication-manager>
<bean id="casAuthenticationProvider"
class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="authenticationUserDetailsService">
<bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<constructor-arg ref="userService" />
</bean>
</property>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.apereo.cas.client.validation.Cas20ServiceTicketValidator">
<constructor-arg index="0" value="https://:9443/cas" />
</bean>
</property>
<property name="key" value="an_id_for_this_auth_provider_only"/>
</bean>
<security:user-service id="userService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used.
This is not safe for production, but makes reading
in samples easier.
Normally passwords should be hashed using BCrypt -->
<security:user name="joe" password="{noop}joe" authorities="ROLE_USER" />
...
</security:user-service>
CasAuthenticationProvider 使用 UserDetailsService 例項來載入使用者透過 CAS 認證後的許可權。我們在此處展示了一個簡單的記憶體設定。請注意,CasAuthenticationProvider 實際上不使用密碼進行認證,但它確實使用許可權。
如果您回顧 CAS 工作原理部分,這些 bean 都相當自解釋。
這完成了 CAS 的最基本配置。如果您沒有犯任何錯誤,您的 Web 應用程式應該可以在 CAS 單點登入框架內愉快地工作。Spring Security 的其他部分無需擔心 CAS 處理身份驗證的事實。在以下部分中,我們將討論一些(可選的)更高階的配置。
單點登出
CAS 協議支援單點登出,並且可以輕鬆新增到您的 Spring Security 配置中。以下是處理單點登出的 Spring Security 配置更新
<security:http entry-point-ref="casEntryPoint">
...
<security:logout logout-success-url="/cas-logout.jsp"/>
<security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<security:custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</security:http>
<!-- This filter handles a Single Logout Request from the CAS Server -->
<bean id="singleLogoutFilter" class="org.apereo.cas.client.session.SingleSignOutFilter"/>
<!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
<bean id="requestSingleLogoutFilter"
class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg value="https://:9443/cas/logout"/>
<constructor-arg>
<bean class=
"org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</constructor-arg>
<property name="filterProcessesUrl" value="/logout/cas"/>
</bean>
logout 元素將使用者從本地應用程式登出,但不會結束與 CAS 伺服器或已登入的其他應用程式的會話。requestSingleLogoutFilter 過濾器將允許請求 URL /spring_security_cas_logout 以將應用程式重定向到已配置的 CAS 伺服器登出 URL。然後,CAS 伺服器將向所有已登入的服務傳送單點登出請求。singleLogoutFilter 透過在靜態 Map 中查詢 HttpSession 然後使其失效來處理單點登出請求。
為什麼 logout 元素和 singleLogoutFilter 都需要,這可能令人困惑。首先進行本地登出被認為是最佳實踐,因為 SingleSignOutFilter 只是將 HttpSession 儲存在一個靜態 Map 中,以便呼叫其 invalidate 方法。根據上述配置,登出流程將是:
-
使用者請求
/logout,這將使使用者從本地應用程式登出並將其傳送到登出成功頁面。 -
登出成功頁面
/cas-logout.jsp應指示使用者單擊指向/logout/cas的連結以登出所有應用程式。 -
當用戶點選連結時,使用者將被重定向到 CAS 單點登出 URL (localhost:9443/cas/logout)。
-
在 CAS 伺服器端,CAS 單點登出 URL 然後向所有 CAS 服務提交單點登出請求。在 CAS 服務端,Apereo 的
SingleSignOutFilter透過使原始會話失效來處理登出請求。
下一步是將以下內容新增到您的 web.xml
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
org.apereo.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>
使用 SingleSignOutFilter 時,您可能會遇到一些編碼問題。因此,建議新增 CharacterEncodingFilter 以確保在使用 SingleSignOutFilter 時字元編碼正確。再次,請參閱 Apereo CAS 的文件以獲取詳細資訊。SingleSignOutHttpSessionListener 確保當 HttpSession 過期時,用於單點登出的對映被移除。
使用 CAS 對無狀態服務進行身份驗證
本節描述如何使用 CAS 對服務進行身份驗證。換句話說,本節討論如何設定使用 CAS 進行身份驗證的客戶端。下一節描述如何設定使用 CAS 進行身份驗證的無狀態服務。
配置 CAS 以獲取代理授權票據
為了向無狀態服務進行身份驗證,應用程式需要獲取代理授予票據 (PGT)。本節描述如何配置 Spring Security 來獲取 PGT,它建立在 cas-st[服務票據認證]配置之上。
第一步是在您的 Spring Security 配置中包含一個 ProxyGrantingTicketStorage。這用於儲存由 CasAuthenticationFilter 獲取的 PGT,以便它們可以用於獲取代理票據。示例如下所示
<!--
NOTE: In a real application you should not use an in memory implementation.
You will also want to ensure to clean up expired tickets by calling
ProxyGrantingTicketStorage.cleanup()
-->
<bean id="pgtStorage" class="org.apereo.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>
下一步是更新 CasAuthenticationProvider 以便能夠獲取代理票據。為此,將 Cas20ServiceTicketValidator 替換為 Cas20ProxyTicketValidator。proxyCallbackUrl 應設定為應用程式將接收 PGT 的 URL。最後,配置還應引用 ProxyGrantingTicketStorage,以便它可以使用 PGT 獲取代理票據。您可以在下面找到應進行的配置更改示例。
<bean id="casAuthenticationProvider"
class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
<bean class="org.apereo.cas.client.validation.Cas20ProxyTicketValidator">
<constructor-arg value="https://:9443/cas"/>
<property name="proxyCallbackUrl"
value="https://:8443/cas-sample/login/cas/proxyreceptor"/>
<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
</bean>
</property>
</bean>
最後一步是更新 CasAuthenticationFilter 以接受 PGT 並將其儲存在 ProxyGrantingTicketStorage 中。重要的是 proxyReceptorUrl 必須與 Cas20ProxyTicketValidator 的 proxyCallbackUrl 匹配。下面顯示了一個示例配置。
<bean id="casFilter"
class="org.springframework.security.cas.web.CasAuthenticationFilter">
...
<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
<property name="proxyReceptorUrl" value="/login/cas/proxyreceptor"/>
</bean>
使用代理票據呼叫無狀態服務
既然 Spring Security 獲取 PGT,您可以使用它們建立代理票據,這些票據可用於對無狀態服務進行身份驗證。CAS 示例應用程式在 ProxyTicketSampleServlet 中包含一個工作示例。示例程式碼如下所示
-
Java
-
Kotlin
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal();
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl);
// Make a remote call using the proxy ticket
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8");
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
...
}
protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?) {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
val token = request.userPrincipal as CasAuthenticationToken
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
val proxyTicket = token.assertion.principal.getProxyTicketFor(targetUrl)
// Make a remote call using the proxy ticket
val serviceUrl: String = targetUrl + "?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8")
val proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8")
}
代理票據認證
CasAuthenticationProvider 區分有狀態和無狀態客戶端。有狀態客戶端是指任何向 CasAuthenticationFilter 的 filterProcessesUrl 提交請求的客戶端。無狀態客戶端是指任何向 CasAuthenticationFilter 提交身份驗證請求的客戶端,其 URL 不是 filterProcessesUrl。
因為遠端協議無法在 HttpSession 的上下文中呈現自身,所以無法依賴在請求之間將會話中的安全上下文儲存在會話中的預設做法。此外,由於 CAS 伺服器在 TicketValidator 驗證票據後使其失效,因此在後續請求中呈現相同的代理票據將不起作用。
一個明顯的選擇是根本不為遠端協議客戶端使用 CAS。然而,這將消除 CAS 的許多理想功能。作為一種折衷方案,CasAuthenticationProvider 使用 StatelessTicketCache。這僅用於無狀態客戶端,這些客戶端使用等於 CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER 的主體。發生的情況是 CasAuthenticationProvider 會將生成的 CasAuthenticationToken 儲存在 StatelessTicketCache 中,以代理票據為鍵。因此,遠端協議客戶端可以呈現相同的代理票據,CasAuthenticationProvider 將無需聯絡 CAS 伺服器進行驗證(除了第一個請求)。一旦透過身份驗證,代理票據可用於除原始目標服務之外的 URL。
本節建立在前面各節的基礎上,以適應代理票據認證。第一步是指定認證所有工件,如下所示。
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
...
<property name="authenticateAllArtifacts" value="true"/>
</bean>
下一步是為 CasAuthenticationFilter 指定 serviceProperties 和 authenticationDetailsSource。serviceProperties 屬性指示 CasAuthenticationFilter 嘗試認證所有工件,而不僅僅是 filterProcessesUrl 上存在的工件。ServiceAuthenticationDetailsSource 建立一個 ServiceAuthenticationDetails,確保在驗證票據時,基於 HttpServletRequest 的當前 URL 用作服務 URL。生成服務 URL 的方法可以透過注入自定義 AuthenticationDetailsSource 返回自定義 ServiceAuthenticationDetails 來進行自定義。
<bean id="casFilter"
class="org.springframework.security.cas.web.CasAuthenticationFilter">
...
<property name="serviceProperties" ref="serviceProperties"/>
<property name="authenticationDetailsSource">
<bean class=
"org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource">
<constructor-arg ref="serviceProperties"/>
</bean>
</property>
</bean>
您還需要更新 CasAuthenticationProvider 以處理代理票據。為此,請將 Cas20ServiceTicketValidator 替換為 Cas20ProxyTicketValidator。您需要配置 statelessTicketCache 以及您希望接受的代理。您可以在下面找到接受所有代理所需的更新示例。
<bean id="casAuthenticationProvider"
class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
<bean class="org.apereo.cas.client.validation.Cas20ProxyTicketValidator">
<constructor-arg value="https://:9443/cas"/>
<property name="acceptAnyProxy" value="true"/>
</bean>
</property>
<property name="statelessTicketCache">
<bean class="org.springframework.security.cas.authentication.SpringCacheBasedTicketCache">
<property name="cache">
<bean class="net.sf.ehcache.Cache"
init-method="initialise" destroy-method="dispose">
<constructor-arg value="casTickets"/>
<constructor-arg value="50"/>
<constructor-arg value="true"/>
<constructor-arg value="false"/>
<constructor-arg value="3600"/>
<constructor-arg value="900"/>
</bean>
</property>
</bean>
</property>
</bean>