天天看點

Spring Security(20)——整合Cas

目錄

<a href="#_Toc440490838">1.1           配置登入認證</a>

<a href="#_Toc440490839">1.1.1     配置AuthenticationEntryPoint</a>

<a href="#_Toc440490840">1.1.2     配置CasAuthenticationFilter</a>

<a href="#_Toc440490841">1.1.3     配置AuthenticationManager</a>

<a href="#_Toc440490842">1.2           單點登出</a>

<a href="#_Toc440490843">1.3           使用代理</a>

<a href="#_Toc440490844">1.3.1     代理端</a>

<a href="#_Toc440490845">1.3.2     被代理端</a>

<a href="#_Toc440490846">1.3.3     既為代理端又為被代理端</a>

       衆所周知,Cas是對單點登入的一種實作。本文假設讀者已經了解了Cas的原理及其使用,這些内容在本文将不會讨論。Cas有Server端和Client端,Client端通常對應着我們自己的應用,Spring Security整合Cas指的就是在Spring Security應用中整合Cas Client,已達到使用Cas Server實作單點登入和登出的效果。本文旨在描述如何在Spring Security應用中使用Cas的單點登入。

       首先需要将Spring Security對Cas支援的jar包加入到應用的類路徑中。如果我們的應用使用Maven構造的,則可以在應用的pom.xml檔案中加上如下依賴。

   &lt;dependency&gt;

      &lt;groupId&gt;org.springframework.security&lt;/groupId&gt;

      &lt;artifactId&gt;spring-security-cas&lt;/artifactId&gt;

      &lt;version&gt;${spring.security.version}&lt;/version&gt;

   &lt;/dependency&gt;

       加入了spring-security-cas-xxx.jar到Spring Security應用的classpath後,我們便可以開始配置我們的Spring Security應用使用Cas進行單點登入了。

       首先需要做的是将應用的登入認證入口改為使用CasAuthenticationEntryPoint。是以首先我們需要配置一個CasAuthenticationEntryPoint對應的bean,然後指定需要進行登入認證時使用該AuthenticationEntryPoint。配置CasAuthenticationEntryPoint時需要指定一個ServiceProperties,該對象主要用來描述service(Cas概念)相關的屬性,主要是指定在Cas Server認證成功後将要跳轉的位址。

   &lt;!-- 指定登入入口為casEntryPoint --&gt;

   &lt;security:http  entry-point-ref="casEntryPoint"&gt;

      ...

   &lt;/security:http&gt;

   &lt;!-- 認證的入口 --&gt;

   &lt;bean id="casEntryPoint"

      class="org.springframework.security.cas.web.CasAuthenticationEntryPoint"&gt;

      &lt;!-- Cas Server的登入位址,elim是我的計算機名 --&gt;

      &lt;property name="loginUrl" value="https://elim:8443/cas/login" /&gt;

      &lt;!-- service相關的屬性 --&gt;

      &lt;property name="serviceProperties" ref="serviceProperties" /&gt;

   &lt;/bean&gt;

   &lt;!-- 指定service相關資訊 --&gt;

   &lt;bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties"&gt;

      &lt;!-- Cas Server認證成功後的跳轉位址,這裡要跳轉到我們的Spring Security應用,之後會由CasAuthenticationFilter處理,預設處理位址為/j_spring_cas_security_check --&gt;

      &lt;property name="service"

         value="http://elim:8080/app/j_spring_cas_security_check" /&gt;

       之後我們需要配置一個CasAuthenticationFilter,并将其放置在Filter連結清單中CAS_FILTER的位置,以處理Cas Server認證成功後的頁面跳轉,用以在Spring Security中進行認證。該Filter會将Cas Server傳遞過來的ticket(Cas概念)封裝成一個Authentication(對應UsernamePasswordAuthenticationToken,其中ticket作為該Authentication的password),然後傳遞給AuthenticationManager進行認證。

   &lt;security:http entry-point-ref="casEntryPoint"&gt;

      &lt;security:custom-filter ref="casFilter" position="CAS_FILTER"/&gt;

   &lt;bean id="casFilter"

      class="org.springframework.security.cas.web.CasAuthenticationFilter"&gt;

      &lt;property name="authenticationManager" ref="authenticationManager" /&gt;

      &lt;!-- 指定處理位址,不指定時預設将會是“/j_spring_cas_security_check” --&gt;

      &lt;property name="filterProcessesUrl" value="/j_spring_cas_security_check"/&gt;

       CasAuthenticationFilter會将封裝好的包含Cas Server傳遞過來的ticket的Authentication對象傳遞給AuthenticationManager進行認證。我們知道預設的AuthenticationManager實作類為ProviderManager,而ProviderManager中真正進行認證的是AuthenticationProvider。是以接下來我們要在AuthenticationManager中配置一個能夠處理CasAuthenticationFilter傳遞過來的Authentication對象的AuthenticationProvider實作,CasAuthenticationProvider。CasAuthenticationProvider首先會利用TicketValidator(Cas概念)對Authentication中包含的ticket資訊進行認證。認證通過後将利用持有的AuthenticationUserDetailsService根據認證通過後回傳的Assertion對象中擁有的username加載使用者對應的UserDetails,即主要是加載使用者的相關權限資訊GrantedAuthority。然後構造一個CasAuthenticationToken進行傳回。之後的邏輯就是正常的Spring Security的邏輯了。

   &lt;security:authentication-manager alias="authenticationManager"&gt;

      &lt;security:authentication-provider ref="casAuthenticationProvider"/&gt;

   &lt;/security:authentication-manager&gt;

   &lt;bean id="casAuthenticationProvider"

   class="org.springframework.security.cas.authentication.CasAuthenticationProvider"&gt;

      &lt;!-- 通過username來加載UserDetails --&gt;

      &lt;property name="authenticationUserDetailsService"&gt;

         &lt;bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"&gt;

            &lt;!-- 真正加載UserDetails的UserDetailsService實作 --&gt;

            &lt;constructor-arg ref="userDetailsService" /&gt;

         &lt;/bean&gt;

      &lt;/property&gt;

      &lt;!-- 配置TicketValidator在登入認證成功後驗證ticket --&gt;

      &lt;property name="ticketValidator"&gt;

         &lt;bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator"&gt;

            &lt;!-- Cas Server通路位址的字首,即根路徑--&gt;

            &lt;constructor-arg index="0" value="https:// elim:8443/cas" /&gt;

      &lt;property name="key" value="key4CasAuthenticationProvider" /&gt;

   &lt;bean id="userDetailsService"

      class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"&gt;

      &lt;property name="dataSource" ref="dataSource" /&gt;

       經過以上三步配置以後,我們的Spring Security應用就已經跟Cas整合好了,可以在需要登入的時候通過Cas Server進行單點登入了。

       Spring Security應用整合Cas Client配置單點登出功能實際和單獨使用Cas Client配置單點登出功能一樣,其根本都是通過配置一個SingleSignOutFilter響應Cas Server單點登出時的回調,配置一個SingleSignOutHttpSessionListener用于在Session過期時删除SingleSignOutFilter存放的對應資訊。SingleSignOutFilter需要配置在Cas 的AuthenticationFilter之前,對于Spring Security應用而言,該Filter通常是配置在Spring Security的配置檔案中,而且是配置在CAS_FILTER之前。是以我們可以在Spring Security的配置檔案中進行如下配置。

      &lt;!-- SingleSignOutFilter放在CAS_FILTER之前 --&gt;

      &lt;security:custom-filter ref="casLogoutFilter" before="CAS_FILTER"/&gt;

   &lt;bean id="casLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/&gt;

       然後跟單獨使用Cas Client一樣,在web.xml檔案中配置一個SingleSignOutHttpSessionListener。

   &lt;listener&gt;

   &lt;listener-class&gt;org.jasig.cas.client.session.SingleSignOutHttpSessionListener&lt;/listener-class&gt;

   &lt;/listener&gt;

       經過以上配置在通路Cas Server的logout位址(如:https:elim:8443/cas/logout)進行登出時,Cas Server登出後将回調其中注冊的每一個Service(Cas概念,即client應用),此時在client應用中配置好的SingleSignOutFilter将處理對應Client應用的登出操作。

       雖然以上配置可以滿足我們在Spring Security應用中的單點登出要求,但Cas官方文檔和Spring Security官方文檔都推薦我們在Cas Client應用進行登出操作時,不是直接通路Cas Server的logout,而是先登出本應用,然後告訴使用者其目前登出的隻是本應用,再提供一個對應Cas Server的連結,使其可以進行真正的單點登出。對此,Spring Security官方文檔中給我們提供例子是提供兩個LogoutFilter,一個是登出目前Spring Security應用,一個是登出Cas Server的。

      &lt;!-- 請求登出Cas Server的過濾器,放在Spring Security的登出過濾器之前 --&gt;

      &lt;security:custom-filter ref="requestCasLogoutFilter" before="LOGOUT_FILTER"/&gt;

   &lt;bean id="requestCasLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter"&gt;

      &lt;!-- 指定登出成功後需要跳轉的位址,這裡指向Cas Server的登出URL,以實作單點登出 --&gt;

      &lt;constructor-arg value="https://elim:8443/cas/logout"/&gt;

       &lt;constructor-arg&gt;

         &lt;bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/&gt;

       &lt;/constructor-arg&gt;

       &lt;!-- 該Filter需要處理的位址,預設是Spring Security的預設登出位址“/j_spring_security_logout”--&gt;

       &lt;property name="filterProcessesUrl" value="/j_spring_cas_security_logout"/&gt;

       此外,Spring Security推薦我們在使用Cas Server的單點登出時一起使用CharacterEncodingFilter,以避免SingleSignOutFilter在擷取參數時出現編碼問題。

       關于Cas應用使用代理的基本原理、概念等内容的介紹不在本文讨論範圍之内,如需了解請讀者參考其它資料,或者參考我的另一篇博文。本文旨在描述Spring Security應用在整合Cas後如何通過Cas Proxy通路另一個受Cas包含的應用。

       使用Cas Proxy時有兩個主體,代理端和被代理端。而且我們知道代理端和被代理端針對Cas20ProxyReceivingTicketValidationFilter的配置是不一樣的,雖然整合Cas的Spring Security應用不再使用Cas20ProxyReceivingTicketValidationFilter了,但其底層的核心機制是一樣的。是以Cas整合Spring Security後的應用在作為代理端和被代理端時的配置也是不一樣的。接下來将分開講解Spring Security應用作為代理端和被代理端整合Cas後的配置。

       首先需要為CasAuthenticationFilter多指定兩個參數,proxyReceptorUrl和proxyGrantingTicketStorage。proxyReceptorUrl用以指定Cas Server在回調代理端傳遞pgtId和pgtIou時回調位址相對于代理端的路徑,如“/proxyCallback”,CasAuthenticationFilter會根據proxyReceptorUrl來确定一個請求是否來自Cas Server針對proxy的回調。如果是則需要接收Cas Server傳遞過來的pgtId和pgtIou,并将它們儲存在持有的ProxyGrantingTicketStorage中。CasAuthenticationProvider之後會從ProxyGrantingTicketStorage中擷取對應的pgtId,即proxy granting ticket,并将其儲存在AttributePrincipal中,而AttributePrincipal又會儲存到對應的Assertion中。

   &lt;!-- 配置ProxyGrantingTicketStorage,用以儲存pgtId和pgtIou --&gt;

   &lt;bean id="proxyGrantingTicketStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/&gt;

      &lt;property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage"/&gt;

      &lt;property name="proxyReceptorUrl" value="/proxyCallback"/&gt;

       其次是需要将CasAuthenticationProvider持有的TicketValidator由Cas20ServiceTicketValidator改成Cas20ProxyTicketValidator。其需要配置一個ProxyGrantingTicketStorage用來擷取proxy granting ticket,即我們熟知的pgtId。在單獨使用Cas Proxy時,Cas20ProxyReceivingTicketValidationFilter内部預設持有一個ProxyGrantingTicketStorage實作,其使用的Cas20ProxyTicketValidator也将使用該ProxyGrantingTicketStorage。整合Spring Security之後, Spring Security不使用Cas20ProxyReceivingTicketValidationFilter,而直接由CasAuthenticationFilter擷取proxy granting ticket,由CasAuthenticationProvider對ticket進行校驗。Cas20ProxyTicketValidator内部沒預設的ProxyGrantingTicketStorage,是以在配置Cas20ProxyTicketValidator時我們需要給其指定一個ProxyGrantingTicketStorage實作。此外還需要為Cas20ProxyTicketValidator指定一個proxyCallbackUrl用以指定在Cas20ProxyTicketValidator通過Cas Server校驗service ticket成功後将回調哪個位址以傳遞pgtId和pgtIou。proxyCallbackUrl預設情況下必須使用https協定,而應用的其它請求可以用非https協定。其它的配置和Cas20ServiceTicketValidator一樣,Cas20ProxyTicketValidator的父類其實就是Cas20ServiceTicketValidator。

         &lt;bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator"&gt;

            &lt;!-- Cas Server通路位址的字首,即根路徑--&gt;

            &lt;constructor-arg index="0" value="https://elim:8443/cas" /&gt;

            &lt;!-- 指定Cas Server回調傳遞pgtId和pgtIou的位址,該位址必須使用https協定 --&gt;

            &lt;property name="proxyCallbackUrl" value="https://elim:8043/app/proxyCallback"/&gt;

            &lt;property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage"/&gt;

       經過以上步驟後我們整合Cas後的Spring Security應用就可以作為代理端使用Cas proxy通路其它被Cas保護的應用了,當然前提是其它被代理端能夠接受我們應用的代理,了解Cas Proxy的人應該都知道這一點,在接下來的Spring Security應用整合Cas作為被代理端中也會講到這部分内容。這裡我們假設現在有一個應用app2能夠接受我們應用的代理通路,那麼在基于上述配置的應用中我們可以通過如下代碼通路app2。

@Controller

@RequestMapping("/cas/test")

publicclass CasTestController {

   @RequestMapping("/getData")

   publicvoid getDataFromApp(PrintWriter writer) throws Exception {

      //1、從SecurityContextHolder擷取到目前的Authentication對象,其是一個CasAuthenticationToken

      CasAuthenticationToken cat = (CasAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();

      //2、擷取到AttributePrincipal對象

      AttributePrincipal principal = cat.getAssertion().getPrincipal();

      //3、擷取對應的proxy ticket

      String proxyTicket = principal.getProxyTicketFor("http://elim:8081/app2/getData.jsp");

      //4、請求被代理應用時将擷取到的proxy ticket以參數ticket進行傳遞

      URL url = new URL("http://elim:8081/app2/getData.jsp?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8"));

      HttpURLConnection conn = (HttpURLConnection)url.openConnection();

      BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));

      StringBuffer content = new StringBuffer();

      String line = null;

      while ((line=br.readLine()) != null) {

         content.append(line).append("&lt;br/&gt;");

      }

      writer.write(content.toString());

   }

}

       需要注意的是通過AttributePrincipal的getProxyTicketFor()方法擷取proxy ticket時,每調用一次都會擷取一個全新的proxy ticket。使用者可以根據自己的需要将擷取到的proxy ticket按照指定的URL緩存起來,以避免每次都去針對同一個URL擷取一個全新的proxy ticket。此外,如果在被代理端認證時根據proxy ticket緩存了Authentication的話也需要我們在代理端保證針對同一URL傳遞過去的proxy ticket是一樣的,否則被代理端針對proxy ticket緩存Authentication的功能就沒用了。

       Spring Security應用整合Cas使用Cas Proxy作為被代理端時主要需要進行三點修改。

       第一點是通過ServiceProperties指定CasAuthenticationFilter的authenticateAllArtifacts為true,這樣CasAuthenticationFilter将會嘗試對所有ticket進行認證,而不是隻認證來自filterProccessUrl指定位址的請求。這樣代理端在請求被代理端的資源時将proxy ticket以參數ticket進行傳遞時,CasAuthenticationFilter才會讓CasAuthenticationProvider對proxy ticket進行校驗,這樣我們的請求才有可能被CasAuthenticationProvider認證成功并請求到真正的資源。

       第二點是指定CasAuthenticationFilter使用的AuthenticationDetailsSource為ServiceAuthenticationDetailsSource。CasAuthenticationFilter預設使用的是WebAuthenticationDetailsSource。ServiceAuthenticationDetailsSource将建構一個ServiceAuthenticationDetails對象作為目前Authentication的details對象。ServiceAuthenticationDetailsSource建構的ServiceAuthenticationDetails對象會将目前請求的位址建構為一個serviceUrl,通過其getServiceUrl()方法可以擷取到該serviceUrl位址。之後該Authentication對象傳遞到CasAuthenticationProvider進行認證時就可以從Authentication的details中擷取到對應的serviceUrl,并在通過Cas Server對代理端以參數ticket傳遞過來的proxy ticket進行驗證時連同對應的serviceUrl一起傳遞過去。因為之前代理端申請proxy ticket時就是通過該serviceUrl進行申請的,Cas Server需要對于它們的配對來驗證對應的proxy ticket是否有效。

       第三點是将CasAuthenticationProvider的TicketValidator由Cas20ServiceTicketValidator改為Cas20ProxyTicketValidator,因為Cas Proxy被代理端需要調用Cas Server的proxyValidator對代理端傳遞過來的proxy ticket進行驗證。此外需要通過acceptAnyProxy或allowedProxyChains指定将接受哪些代理。acceptAnyProxy用以指定是否接受所有的代理,可選值為true或false。allowedProxyChains則用以指定具體接受哪些代理,其對應的值是代理端在擷取pgtId時提供給Cas Server的回調位址,如我們需要接受前面示例中代理端的代理,則我們的allowedProxyChains的值應該是“https://elim:8043/app/proxyCallback”。如果需要接受多個代理端的代理,則在指定allowedProxyChains時多個代理端回調位址應各占一行。

       針對以上三點,我們的Spring Security應用整合Cas作為Cas Proxy的被代理端時需要對我們的配置進行如下改造。

         value="http://elim:8083/app2/j_spring_cas_security_check" /&gt;

      &lt;!-- 通過ServiceProperties指定CasAuthenticationFilter的authenticateAllArtifacts為true --&gt;

      &lt;property name="authenticateAllArtifacts" value="true"/&gt;

      &lt;property name="filterProcessesUrl" value="/j_spring_cas_security_check" /&gt;

      &lt;!-- 通過ServiceProperties指定CasAuthenticationFilter的authenticateAllArtifacts為true  --&gt;

      &lt;!-- 指定使用的AuthenticationDetailsSource為ServiceAuthenticationDetailsSource --&gt;

      &lt;property name="authenticationDetailsSource"&gt;

         &lt;bean class="org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource" /&gt;

            &lt;property name="allowedProxyChains"&gt;

                &lt;value&gt;https://elim:8043/app/proxyCallback&lt;/value&gt;

            &lt;/property&gt;

       此外,對于被代理端而言,代理端在對其進行通路時都被認為是無狀态的。對于無狀态的認證CasAuthenticationProvider将在認證成功後将對應的Authentication對象以proxy tickit為key存放到所持有的StatelessTicketCache中,然後在下次代理端通路時将優先根據代理端傳遞過來的proxy ticket從StatelessTicketCache中擷取Authentication對象,如果存在則不再進行認證,否則将繼續進行認證。CasAuthenticationProvider預設持有的StatelessTicketCache為NullStatelessTicketCache,其所有的實作都是空的。是以預設情況下,被代理端在被代理端通路時将每次都對代理端進行認證。如果使用者不希望在被代理端每次都對代理端的請求進行認證,則可以為被代理端的CasAuthenticationProvider指定一個StatelessTicketCache。使用者可以實作自己的StatelessTicketCache,并指定CasAuthenticationProvider使用的StatelessTicketCache為該StatelessTicketCache。不過也可以使用Spring Security為我們提供的EhCacheBasedTicketCache。EhCacheBasedTicketCache是基于Ehcache實作的一個StatelessTicketCache。以下是一個為CasAuthenticationProvider配置EhCacheBasedTicketCache的示例。

……

      &lt;property name="statelessTicketCache"&gt;

         &lt;bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache"&gt;

            &lt;!-- Ehcache對象 --&gt;

            &lt;property name="cache" ref="proxyTicketCache"/&gt;

   &lt;!-- 定義一個Ehcache --&gt;

   &lt;bean id="proxyTicketCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"&gt;

      &lt;property name="cacheName" value="proxyTicketCache" /&gt;

      &lt;property name="timeToLive" value="600"/&gt;

       需要注意的是在代理端通過AttributePrincipal的getProxyTicketFor()方法擷取到的proxy ticket每次都是不一樣的,是以在被代理端通過StatelessTicketCache根據proxy ticket緩存認證對象Authentication時隻有在同一proxy ticket能夠請求多次的情況下才會有用,這也就要求我們在代理端同樣能将proxy ticket緩存起來,以在請求同一位址時能使用相同的proxy ticket。

       Cas Proxy的代理端和被代理端是互相獨立的,是以一個Cas應用既可以作為代理端去通路其它Cas應用,也可以作為被代理端被其它應用通路。當Spring Security應用整合Cas後既想作為Cas Proxy的代理端通路其它Cas應用,也想作為被代理端被其它Cas應用通路時隻需要将上述作為代理端的配置和作為被代理端的配置整到一起就行了。以下是一段示例代碼。

         value="http://elim:8080/app /j_spring_cas_security_check" /&gt;

            &lt;!-- 作為被代理端時配置接收任何代理 --&gt;

            &lt;property name="acceptAnyProxy" value="true"/&gt;

       關于Cas的更多内容可以參考Cas的官方文檔,或者參考我的其它關于Cas的部落格。

(注:本文是基于Spring Security3.1.6、Cas Server3.5.2所寫)