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 應用程式被稱為“服務”。服務有三種類型。認證服務票據(service tickets)的服務,能夠獲取代理票據(proxy tickets)的服務,以及認證代理票據的服務。認證代理票據有所不同,因為必須驗證代理列表,而且代理票據通常可以重複使用。
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
。principal(主體)將等於CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
,而 credentials(憑據)將是服務票據的不透明值。然後,此認證請求將交給配置的AuthenticationManager
。 -
AuthenticationManager
實現將是ProviderManager
,它又配置了CasAuthenticationProvider
。CasAuthenticationProvider
只響應包含 CAS 特定 principal(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
)的UsernamePasswordAuthenticationToken
和CasAuthenticationToken
(稍後討論)。 -
CasAuthenticationProvider
將使用TicketValidator
實現驗證服務票據。這通常是Cas20ServiceTicketValidator
,它是 CAS 客戶端庫中包含的類之一。如果應用程式需要驗證代理票據,則使用Cas20ProxyTicketValidator
。TicketValidator
向 CAS 伺服器傳送 HTTPS 請求以驗證服務票據。它也可能包含一個代理回撥 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 的 示例 中找到。
Service Ticket 認證
本節描述如何設定 Spring Security 以認證 Service Ticket。通常,這已經是 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
例項來載入使用者的許可權(authorities),一旦他們透過 CAS 認證。這裡我們展示了一個簡單的記憶體配置。請注意,CasAuthenticationProvider
實際上不使用密碼進行認證,但它會使用許可權。
如果您回顧 CAS 工作原理 部分,這些 Bean 都相當容易理解。
這完成了 CAS 最基本的配置。如果您沒有犯任何錯誤,您的 Web 應用程式應該可以在 CAS 單點登入框架內正常工作。Spring Security 的其他部分無需關心 CAS 處理了認證的事實。在以下部分,我們將討論一些(可選的)更高階的配置。
單點登出
CAS 協議支援單點登出 (Single Logout),並且可以輕鬆新增到您的 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 Server 登出 URL。然後,CAS Server 將向所有已登入的服務傳送單點登出請求。singleLogoutFilter
透過在靜態 Map
中查詢 HttpSession
,然後使其失效來處理單點登出請求。
可能會令人困惑的是為什麼同時需要 logout
元素和 singleLogoutFilter
。最好先在本地登出,因為 SingleSignOutFilter
只將 HttpSession
儲存在一個靜態 Map
中,以便對其呼叫 invalidate。使用上述配置,登出流程將是:
-
使用者請求
/logout
,這將使該使用者從本地應用程式中登出,並將使用者傳送到登出成功頁面。 -
登出成功頁面
/cas-logout.jsp
應指導使用者單擊指向/logout/cas
的連結,以登出所有應用程式。 -
當用戶單擊連結時,使用者將被重定向到 CAS 單點登出 URL (localhost:9443/cas/logout)。
-
在 CAS Server 端,CAS 單點登出 URL 然後向所有 CAS 服務提交單點登出請求。在 CAS Service 端,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 以獲取代理授權票據 (Proxy Granting Ticket)
為了向無狀態服務進行認證,應用程式需要獲取一個代理授權票據 (PGT)。本節描述如何在 Spring Security 中配置以獲取 PGT,並以上一節的 CAS Service Ticket 認證配置為基礎。
第一步是在您的 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>
使用代理票據 (Proxy Ticket) 呼叫無狀態服務
現在 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")
}
Proxy Ticket 認證
CasAuthenticationProvider
區分有狀態和無狀態客戶端。有狀態客戶端被認為是提交到 CasAuthenticationFilter
的 filterProcessesUrl
的任何客戶端。無狀態客戶端是向 CasAuthenticationFilter
在非 filterProcessesUrl
的 URL 上提交認證請求的任何客戶端。
由於遠端呼叫協議無法在 HttpSession
的上下文中呈現自身,因此不可能依賴在請求之間將會話中的安全上下文儲存的預設做法。此外,由於 CAS 伺服器在 TicketValidator
驗證票據後會使其失效,因此在後續請求中呈現相同的代理票據將不起作用。
一個顯而易見的選擇是完全不使用 CAS 進行遠端呼叫協議客戶端。然而,這將消除 CAS 的許多可取功能。作為折中方案,CasAuthenticationProvider
使用了一個 StatelessTicketCache
。這僅用於使用等於 CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
的 principal 的無狀態客戶端。發生的情況是 CasAuthenticationProvider
將 resulting 的 CasAuthenticationToken
儲存在 StatelessTicketCache
中,以代理票據為鍵。因此,遠端呼叫協議客戶端可以呈現相同的代理票據,並且 CasAuthenticationProvider
無需聯絡 CAS 伺服器進行驗證(除了第一個請求)。一旦認證成功,代理票據可以用於原始目標服務之外的 URL。
本節在前面的部分的基礎上構建,以適應代理票據認證。第一步是指定認證所有 artifact,如下所示。
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
...
<property name="authenticateAllArtifacts" value="true"/>
</bean>
下一步是為 CasAuthenticationFilter
指定 serviceProperties
和 authenticationDetailsSource
。serviceProperties
屬性指示 CasAuthenticationFilter
嘗試認證所有 artifact,而不僅僅是出現在 filterProcessesUrl
上的 artifact。ServiceAuthenticationDetailsSource
建立一個 ServiceAuthenticationDetails
,該物件確保基於 HttpServletRequest
的當前 URL 用作驗證票據時的服務 URL。生成服務 URL 的方法可以透過注入自定義的 AuthenticationDetailsSource
來自定義,該 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.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>