天天看點

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

【原創申明:文章為原創,歡迎非盈利性轉載,但轉載必須注明來源】

之前寫過一篇文章,介紹單點登入的基本原理。這篇文章重點介紹開源單點登入系統cas的登入和登出的實作方法。并結合實際工作中碰到的問題,探讨在叢集環境中應用單點登入可能會面臨的問題。這篇文章在上一篇的基礎上,增加了第四部分,最終的解決方案。

為了描述友善,假設有如下一個單點登入系統。一套casserver,兩套cas client系統。為了描述的友善,省略cas server調用使用者系統完成登入,以及casclient從使用者系統讀取使用者詳細資訊的過程。

假定有兩個cas client應用,一個cas server。應用的部署,可能在不同的伺服器,也可能有不同的通路ip或域名,即使是同一個浏覽器,在各個應用中的session資訊也是不相同的。

浏覽器中,每個應用有一個獨立的jsessionidcookie。某一個應用,不可能讀取到浏覽器在其他應用中的cookie資訊。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

假定使用者首先通路cas client 01,系統提醒使用者進行一次登入;然後使用者通路cas client2,不會再提示登入而是直接登入成功。

使用者打開浏覽器後第一次通路,重定向到單點登入後,會提示使用者輸入賬号密碼登入。登入成功之後,再跳轉回cas client。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

當使用者浏覽器已經登入系統,切換到另一個casclient時,跟第一次通路有所不同,因為已經登入成功,就不會再提醒輸入賬号密碼登入了。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

當使用者已經通路過cas client後,當使用者再次通路,系統不會再跳轉到cas server做認證。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

為了實作前述的單點登入過程,以java web項目為例,需要在 web.xml 中進行相應的配置。(為了排版,沒有填寫filter的完整class名,請自行查閱補充。)

<filter>

  <filter-name>cas authenticationfilter</filter-name>

  <filter-class>*.authenticationfilter</filter-class>

</filter>

  <filter-name>cas validation filter</filter-name>

  <filter-class>*.cas10ticketvalidationfilter</filter-class>

  <filter-name>cas httpservletrequest wrapperfilter</filter-name>

  <filter-class>*.httpservletrequestwrapperfilter</filter-class>

<filter-mapping>

  <url-pattern>/*</url-pattern>

</filter-mapping>

         <filter-name>cas httpservletrequest wrapperfilter</filter-name>

         <url-pattern>/*</url-pattern>

仔細看一下配置過濾器可以發現,三個過濾器正好對應流程圖中三次通路cas client。

authentication filter:負責将未登入使用者跳轉到登入界面

authentication filter:負責驗證service ticket

httpservletrequest wrapperfilter:負責将使用者資訊封裝到request和session中。

當使用者通路系統後從系統登出,如何能夠從每個應用中都登出?注意前面1.4部分的描述,如果使用者登出時,并沒有登出casclient 02中的會話資訊,如果使用者在浏覽器中直接通路這個應用,因為session存在,并不會提醒使用者重新登入。

這會帶來兩個潛在的隐患:

1、  使用者登出user1後換賬号user2重新登入,進入cas client 02之後,目前身份其實還是user1,并沒有如使用者預期一樣使用user2身份。

2、  使用者user1點選登出後離開,沒有關閉浏覽器。這時候其他使用者直接打開cas client 02,能夠直接盜用user1的身份進行操作。

cas已經考慮到統一登出的問題。

這裡有三個重要的概念tgt、st和service,需要着重介紹一下,因為它們同後續統一登出的方案息息相關。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

這是使用者第一次通路cas client的url。假設一個cas client應用部署在域名oa.company.com,使用http協定,應用首頁是index.htm。當使用者第一次通路這個應用時,對應的url位址是 http://oa.company.com/index.htm 。這個url,對cas server來說,就是一個service。

當使用者第一次跳轉到cas server的時候,可以看到傳了一個參數service,就是這個值。當casserver生成ticket重定向到cas client的時候,實際就是在這個service 中添加了一個參數 ticket 。

tgt是cas server為每一個登入使用者建立的登入令牌。在casserver上擁有了tgt,使用者就可以證明自己在casserver成功登入過。tgt封裝了sessioncookie值以及此cookie值對應的使用者資訊。當http請求到來時,cas以此cookie值為key查詢緩存中有無tgt ,如果有的話,則相信使用者已登入過。

st是cas server為使用者簽發的通路某一service的認證令牌。使用者通路service時,service發現使用者沒有st,浏覽器會跳轉到casserver去擷取st。cas server發現使用者有tgt,則簽發一個st,傳回給使用者。使用者使用st作為ticket參數去通路service,service拿st去cas server驗證,驗證通過後,得到目前登入使用者的登入名。

注意tgt和st,是一對多的關系。一個tgt會維護一個 services 清單,每當為使用者建立一個st并認證通過後,會将這個st添加到tgt的services清單中。這樣,在casserver端,這個services清單實際維護了一個使用者登入過的所有casclient。這就為實作統一登出打下了基礎。

cas client,為了實作統一登出,除了第一張介紹的三個登入過程的過濾器之外,還需要添加一個統一登出過濾器。

 <filter-name>cas single sign outfilter</filter-name>

 <filter-class>*.singlesignoutfilter</filter-class>

 <url-pattern>/*</url-pattern>

<listener>

 <listener-class>*.singlesignouthttpsessionlistener</listener-class>

</listener>

使用者在浏覽器中點選“登出”連結,實際浏覽器會通路casserver的登出頁面。收到登出請求後,cas server會讀取到tgt,并檢查目前使用者登入過的所有service,并依次發送登出請求。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

cas client的登出,核心代碼是singlesignoutfilter,它的關鍵代碼

public voiddofilter(servletrequest, servletresponse, filterchain){

         httpservletrequest request =(httpservletrequest)servletrequest;

         if (handler.istokenrequest(request)) {

                   handler.recordsession(request);

         } else if (handler.islogoutrequest(request)) {

                   handler.destroysession(request);

                   return;

         }

         filterchain.dofilter(servletrequest, servletresponse);

}

其中handler是singlesignouthandler的執行個體,這個對象完成使用者在casclient端登入資訊的維護和登出工作。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

至此,cas完整的登入和登出過程就完成。

統一登出的實作,需要cas server通過httpclient通路cas client的service。如果這個通路過程失敗,就會導緻統一登出失敗。列了幾種情況,不詳述。

1、開發調試階段,使用localhost通路cas client。

2、cas server部署在外網,cas client部署在内網。

3、網絡安全設定,不允許casserver通路cas client。

前面的論述,一直假定所有的cas client都是單點部署,沒有叢集。如果叢集,會有什麼影響,應該如何來解決?

假設使用nginx做叢集前端,後面部署兩台cas client 01的執行個體。我們看看對登入過程會有什麼影響。

為了描述友善,cas client登入過程會有三次請求(對應三個過濾器),我們依次命名為authentication request / validation request / wrapper request。

nginx預設的分發規則,并不是sticky模式,同一個浏覽器的請求,會按照nginx自身某種規則進行分發。我們曾經測試過,在雙點叢集環境下,authentication request和validationrequest會恰好被分發到兩台伺服器,這就會導緻登入過程死循環。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

出現登入死循環的原因,主要在于nginx分發時,沒有使用sticky政策,也就是同一個浏覽器的請求,永遠分發給同一台cas client執行個體。預設nginx的分發政策,可以根據使用者ip分發,實作的是同一個ip永遠分發到同一台client,這樣就能解決死循環的問題。

當nginx實作了sitcky轉發,同一個浏覽器的通路會分發到同一個client1執行個體,該使用者的會話資訊也一直儲存在client1執行個體中。

當使用者統一登出時,由cas server向client發送登出請求,這時候nginx無法確定按目前使用者進行分發,是以可能會被分發到client2。這時候,實際效果是登出失敗。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

這個問題,在我們目前的環境中真實存在,還沒有合理的解決方法。初步分析,大概有幾個修改方向。

問題存在的原因,是因為nginx在分發登出政策時,不能準确分發。如果能在這個環節進行修改,系統代碼和環境,基本不用做任何修改。

這裡有兩種分發方法:

l cas server發送的登出請求,分發給對應的背景伺服器。

l cas server發送的登出請求,廣播到所有的背景伺服器。

初步結論:同架構組進行了溝通,這兩種方案都很難實作,特别是廣播的方案,沒在網絡上找到類似成功的案例。

如果能實作叢集session的同步:同步建立、同步登出,主要在一個client上實作了登出,其他client也就同步登出。

這個會對tomcat性能有影響。

即使是多個節點,它們的會話資訊隻有一份。一旦失效,則所有節點都失效。這隻是一個設想,沒有做技術調研,不知能夠實作。

這有兩種修改方法:

l 修改tomcat的配置檔案,使用redis儲存tomcat的會話資訊。

l 修改代碼而不是tomcat,使用redis儲存會話資訊。

初步結論:架構組不允許修改生産環境的tomcat,否定了第一種方法。我們隻能嘗試修改代碼并利用redis儲存會話。

首先,在cas server中實作一個接口,用于判斷某一個st對應的tgt是否還有效。

在singlesignoutfilter中,每次通路都調用cas server的這個新接口,判斷使用者是否已經登出。如果已經登出,則立刻登出本執行個體中的會話資訊。

這個方法是比較安全的解決辦法,但每次請求都會調用casserver接口,會對性能造成巨大影響。完全不建議用這種方案。

對前面提到的幾種方案做了初步調研之後:

l 技術實作困難,否定了方案1

l 性能考慮以及架構組的政策,否定方案2

l 架構組的政策,否定方案3中的第一種做法。

l 性能考慮,否定方案4。

是以,可能的做法是修改代碼,使用redis儲存會話資訊。

四  使用redis儲存會話

在目前的生産環境的限制下,我們隻能采用修改代碼來實作redis儲存會話的實作方案。

在tomcat預設的實作中,session資訊都是儲存在jvm中,是以不能跨jvm共享。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

要想将所有的session都儲存到redis中,一種能想到的簡單辦法是自己寫一個customsession,将會話資訊儲存到這個自定義的session中,并且利用redis等進行儲存。但這樣做,會帶來很大的代碼改動,所有涉及到session讀寫操作的地方可能都需要修改。

我們希望找到更優雅的解決方案,能夠修改更少的代碼。

request 和session什麼時候建立?如何傳遞?

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

filter的調用入口函數是dofilter,傳入的主要參數是request和response。在此之前,tomcat已經建立好request。通常情況下,業務代碼不需要關心request和session等對象如何建立的問題,隻需要使用即可。每個過濾器的實作,當需要繼續流程的時候,隻需要将得到的request和response傳遞給下一個filter就行。

但這僅僅是預設做法,并不表示我們不能修改或重寫一個request對象。我們想修改session的儲存位置,如果能在所有的filter之前插入一個自定義過濾器,定義一個新的request傳遞給後面的filter,并且讓後面的filter和servlet感受不到變化,就可以實作這個目标。

在所有的filter之前,插入一個新的filter。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

httpservletrequest可以重寫嗎?

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

在session重寫一個redissessionrequest,繼承自httpservletrequestwrapper,并包含原request(requestfacade)的引用。但需要讀取form參數時,直接調用orirequest取值。當需要拿到session對象進行會話資訊通路時,調用重載後的函數。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

這樣就實作了request的封裝,在後續的filter和servlet中通過request擷取到的session,都是放在redis中的會話資料,不再是預設儲存在jvm中的資料。

當nginx将同一個浏覽器的請求分發給不同的tomcat時,都會根據sessionid從redis中讀取session。因為同一個浏覽器發送請求的sessionid相同,是以在不同的tomcat執行個體中,會讀取到同一個session對象。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

根據前面的分析,在項目中自定義request,就可以實作需求。spring session已經是一個成熟的開源實作,并且後端實作了将會話儲存在redis、mongodb、jdbc等多種實作,我們沒必要自己發明輪子。

spring提供的例子代碼很簡潔,跟我們已經實作的業務系統稍微有點不同。在現有系統中,已經定義了bean jedisconnectionfactory,可以直接使用。

在pom.xml檔案中,添加代碼

<dependency>

         <groupid>org.springframework.session</groupid>

         <artifactid>spring-session-data-redis</artifactid>

         <version>1.2.0.release</version>

</dependency>

在項目中已經有redis配置檔案spring-redis.xml,在其中添加内容

<context:annotation-config/>

<beans:beanclass="org.springframework.session.data.redis.config.annotation.web.http.redishttpsessionconfiguration"/>

在所有的過濾器前面添加一個新的過濾器

 <filter-name>springsessionrepositoryfilter</filter-name>

 <filter-class>org.springframework.web.filter.delegatingfilterproxy</filter-class>

  <dispatcher>request</dispatcher>

  <dispatcher>error</dispatcher>

內建spring session後,經過初步測試,能夠達到預想效果。(感謝同僚瑞钊的實際測試并提供截圖)

使用者登入後檢視redis中的資料,可以看到這些session資訊。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響
CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

使用者登入後繼續通路系統,不會切換到cas登入頁面。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

如果手工删掉redis中的session,重新通路,可以看到需要重新做一個cas認證的過程。

CAS Client叢集環境的問題及解決方案1 單點登入的過程2 統一登出的過程3 CAS Client叢集的影響

後續需要部署一套生産環境的叢集環境,驗證統一登出的效果。