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,它又配置了 CasAuthenticationProviderCasAuthenticationProvider 僅回應包含 CAS 特定 principal(例如 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和 CasAuthenticationToken(稍後討論)的 UsernamePasswordAuthenticationToken

  • CasAuthenticationProvider 將使用 TicketValidator 實作來驗證服務票證。這通常會是 Cas20ServiceTicketValidator,它是 CAS 用戶端程式庫中包含的類別之一。如果應用程式需要驗證代理票證,則會使用 Cas20ProxyTicketValidatorTicketValidator 會向 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 將會呼叫已設定的 CasProxyDeciderCasProxyDecider 指示 TicketResponse 中的代理列表是否可為服務接受。Spring Security 提供了幾種實作:RejectProxyTicketsAcceptAnyCasProxyNamedCasProxyDecider。這些名稱基本上是不言自明的,除了 NamedCasProxyDecider,它允許提供受信任代理的 List

  • CasAuthenticationProvider 將會請求 AuthenticationUserDetailsService 載入適用於 Assertion 中包含的使用者的 GrantedAuthority 物件。

  • 如果沒有問題,CasAuthenticationProvider 會建構一個 CasAuthenticationToken,其中包含 TicketResponseGrantedAuthority 中的詳細資訊。

  • 然後控制權返回到 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 替換為 Cas20ProxyTicketValidatorproxyCallbackUrl 應設定為應用程式將接收 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 中。重要的是 proxyReceptorUrlCas20ProxyTicketValidatorproxyCallbackUrl 相符。下面顯示了一個範例設定。

<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 區分有狀態和無狀態用戶端。有狀態用戶端被認為是任何提交到 CasAuthenticationFilterfilterProcessesUrl 的用戶端。無狀態用戶端是任何在 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 指定 servicePropertiesauthenticationDetailsSourceserviceProperties 屬性指示 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>