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 有一個簡單的方法,可以返回一個布林值,表示給定的一組 Credentials 是否有效。您的 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 後,系統會提示他們輸入使用者名稱和密碼。如果使用者提供了一個 session cookie,表明他們之前已登入,則不會再次提示他們登入(此程序有一個例外,我們稍後會介紹)。CAS 將使用上面討論的 PasswordHandler(或 AuthenticationHandler,如果使用 CAS 3.0)來決定使用者名稱和密碼是否有效。
-
登入成功後,CAS 將會將使用者的瀏覽器重新導向回原始服務。它也會包含一個 ticket 參數,這是一個代表「服務票證」的不透明字串。繼續我們之前的範例,瀏覽器重新導向的 URL 可能是 server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ。
-
回到服務 Web 應用程式中,CasAuthenticationFilter 始終監聽對
/login/cas
的請求(這是可設定的,但在本簡介中我們將使用預設值)。處理篩選器將建構一個代表服務票證的UsernamePasswordAuthenticationToken
。principal 將等於CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
,而 credentials 將是服務票證不透明值。然後,此身份驗證請求將被傳遞給已設定的AuthenticationManager
。 -
AuthenticationManager
的實作將是ProviderManager
,它又配置了CasAuthenticationProvider
。CasAuthenticationProvider
僅回應包含 CAS 特定 principal(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
)和CasAuthenticationToken
(稍後討論)的UsernamePasswordAuthenticationToken
。 -
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
代表代理授權票證 IOU。然後,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
,其中包含使用者名稱(強制性)、代理列表(如果涉及任何代理)和代理授權票證 IOU(如果請求了代理回呼)。 -
接下來,
CasAuthenticationProvider
將會呼叫已設定的CasProxyDecider
。CasProxyDecider
指示TicketResponse
中的代理列表是否可為服務接受。Spring Security 提供了幾種實作:RejectProxyTickets
、AcceptAnyCasProxy
和NamedCasProxyDecider
。這些名稱基本上是不言自明的,除了NamedCasProxyDecider
,它允許提供受信任代理的List
。 -
CasAuthenticationProvider
將會請求AuthenticationUserDetailsService
載入適用於Assertion
中包含的使用者的GrantedAuthority
物件。 -
如果沒有問題,
CasAuthenticationProvider
會建構一個CasAuthenticationToken
,其中包含TicketResponse
和GrantedAuthority
中的詳細資訊。 -
然後控制權返回到
CasAuthenticationFilter
,它將建立的CasAuthenticationToken
放置在安全性內容中。 -
使用者的瀏覽器會重新導向至導致
AuthenticationException
的原始頁面(或取決於設定的自訂目的地)。
很高興您還在這裡!現在讓我們看看如何設定它
CAS 用戶端的設定
由於 Spring Security,CAS 的 Web 應用程式端變得容易。假設您已經了解使用 Spring Security 的基礎知識,因此下面不再重複介紹。我們將假設正在使用基於命名空間的設定,並根據需要加入 CAS bean。每個部分都建立在前一個部分之上。完整的 CAS 範例應用程式可以在 Spring Security 範例中找到。
服務票證驗證
本節說明如何設定 Spring Security 以驗證服務票證。通常,這就是 Web 應用程式所需的全部內容。您需要將 ServiceProperties
bean 新增至您的應用程式上下文。這代表您的 CAS 服務
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
<property name="service"
value="https://127.0.0.1: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://127.0.0.1:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>
為了讓 CAS 運作,ExceptionTranslationFilter
必須將其 authenticationEntryPoint
屬性設定為 CasAuthenticationEntryPoint
bean。這可以使用 entry-point-ref 輕鬆完成,如上面的範例所示。CasAuthenticationEntryPoint
必須參考 ServiceProperties
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://127.0.0.1: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://127.0.0.1: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 伺服器或任何其他已登入應用程式的 session。requestSingleLogoutFilter
篩選器將允許請求 /spring_security_cas_logout
的 URL,以將應用程式重新導向至已設定的 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
通過使原始 session 失效來處理登出請求。
下一步是將以下內容新增至您的 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,並以 thencas-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://127.0.0.1:9443/cas"/>
<property name="proxyCallbackUrl"
value="https://127.0.0.1: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
的用戶端。無狀態用戶端是任何在 filterProcessesUrl
以外的 URL 上向 CasAuthenticationFilter
提出身份驗證請求的用戶端。
由於遠端協定無法在 HttpSession
的上下文中呈現自身,因此無法依賴在請求之間將安全性內容儲存在 session 中的預設做法。此外,由於 CAS 伺服器在票證被 TicketValidator
驗證後會使其失效,因此在後續請求中提供相同的代理票證將無法運作。
一個明顯的選項是完全不要對遠端協定用戶端使用 CAS。但是,這將消除 CAS 的許多理想功能。作為折衷方案,CasAuthenticationProvider
使用 StatelessTicketCache
。這僅適用於使用 principal 等於 CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
的無狀態用戶端。發生的情況是 CasAuthenticationProvider
將會將產生的 CasAuthenticationToken
儲存在 StatelessTicketCache
中,並以代理票證為鍵。因此,遠端協定用戶端可以提供相同的代理票證,並且 CasAuthenticationProvider
不需要聯絡 CAS 伺服器進行驗證(除了第一個請求)。一旦通過驗證,代理票證可以用於原始目標服務以外的 URL。
本節以之前的章節為基礎,以容納代理票證驗證。第一步是指定驗證所有 artifacts,如下所示。
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
...
<property name="authenticateAllArtifacts" value="true"/>
</bean>
下一步是為 CasAuthenticationFilter
指定 serviceProperties
和 authenticationDetailsSource
。serviceProperties
屬性指示 CasAuthenticationFilter
嘗試驗證所有 artifacts,而不僅僅是 filterProcessesUrl
上存在的 artifacts。ServiceAuthenticationDetailsSource
建立一個 ServiceAuthenticationDetails
,以確保在驗證票證時,將基於 HttpServletRequest
的當前 URL 用作服務 URL。可以通過注入返回自訂 ServiceAuthenticationDetails
的自訂 AuthenticationDetailsSource
來客製化產生服務 URL 的方法。
<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://127.0.0.1:9443/cas"/>
<property name="acceptAnyProxy" value="true"/>
</bean>
</property>
<property name="statelessTicketCache">
<bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache">
<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>