CAS的登錄和註銷原理

  • 2019 年 10 月 8 日
  • 筆記

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

之前寫過一篇文章,介紹單點登錄的基本原理。這篇文章重點介紹開源單點登錄系統CAS的登錄和註銷的實現方法。並結合實際工作中碰到的問題,探討在集群環境中應用單點登錄可能會面臨的問題。

1 單點登錄的過程

為了描述方便,假設有如下一個單點登錄系統。一套CASServer,兩套CAS Client系統。為了描述的方便,省略CAS Server調用用戶系統完成登錄,以及CASClient從用戶系統讀取用戶詳細資訊的過程。

1.1多應用情況下Session資訊

假定有兩個CAS Client應用,一個CAS Server。應用的部署,可能在不同的伺服器,也可能有不同的訪問IP或域名,即使是同一個瀏覽器,在各個應用中的Session資訊也是不相同的。

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

假定用戶首先訪問CAS Client 01,系統提醒用戶進行一次登錄;然後用戶訪問CAS Client2,不會再提示登錄而是直接登錄成功。

1.2第一次訪問CAS Client 01

用戶打開瀏覽器後第一次訪問,重定向到單點登錄後,會提示用戶輸入帳號密碼登錄。登錄成功之後,再跳轉回CAS Client。

1.3第一次訪問CAS Client 02

當用戶瀏覽器已經登錄系統,切換到另一個CASClient時,跟第一次訪問有所不同,因為已經登錄成功,就不會再提醒輸入帳號密碼登錄了。

1.4再次訪問CAS Clients

當用戶已經訪問過CAS Client後,當用戶再次訪問,系統不會再跳轉到CAS Server做認證。

1.5CASClient配置

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

<filter>

<filter-name>CAS AuthenticationFilter</filter-name>

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

</filter>

<filter>

<filter-name>CAS Validation Filter</filter-name>

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

</filter>

<filter>

<filter-name>CAS HttpServletRequest WrapperFilter</filter-name>

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

</filter>

<filter-mapping>

<filter-name>CAS Validation Filter</filter-name>

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

</filter-mapping>

<filter-mapping>

<filter-name>CAS AuthenticationFilter</filter-name>

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

</filter-mapping>

<filter-mapping>

<filter-name>CAS HttpServletRequest WrapperFilter</filter-name>

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

</filter-mapping>

仔細看一下配置過濾器可以發現,三個過濾器正好對應流程圖中三次訪問CAS Client。

  • Authentication Filter:負責將未登錄用戶跳轉到登錄介面
  • Authentication Filter:負責驗證Service Ticket
  • HttpServletRequest WrapperFilter:負責將用戶資訊封裝到request和session中。

2 統一註銷的過程

2.1不能實現統一註銷會有什麼問題

當用戶訪問系統後從系統註銷,如何能夠從每個應用中都註銷?注意前面1.4部分的描述,如果用戶註銷時,並沒有註銷CASClient 02中的會話資訊,如果用戶在瀏覽器中直接訪問這個應用,因為Session存在,並不會提醒用戶重新登錄。

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

1、 用戶註銷user1後換帳號user2重新登錄,進入CAS Client 02之後,當前身份其實還是user1,並沒有如用戶預期一樣使用user2身份。

2、 用戶user1點擊註銷後離開,沒有關閉瀏覽器。這時候其他用戶直接打開CAS Client 02,能夠直接盜用user1的身份進行操作。

2.2基本概念:TGT和ST

CAS已經考慮到統一註銷的問題。

這裡有三個重要的概念TGT、ST和Service,需要著重介紹一下,因為它們同後續統一註銷的方案息息相關。

2.2.1 Service

這是用戶第一次訪問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 。

2.2.2 TGT:Ticket Grangting Ticket

TGT是CAS Server為每一個登錄用戶創建的登錄令牌。在CASServer上擁有了TGT,用戶就可以證明自己在CASServer成功登錄過。TGT封裝了SessionCookie值以及此Cookie值對應的用戶資訊。當HTTP請求到來時,CAS以此Cookie值為key查詢快取中有無TGT ,如果有的話,則相信用戶已登錄過。

2.2.3 ST:Service Ticket

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。這就為實現統一註銷打下了基礎。

2.3CAS Client的統一註銷配置

CAS Client,為了實現統一註銷,除了第一張介紹的三個登錄過程的過濾器之外,還需要添加一個統一註銷過濾器。

<filter>

<filter-name>CAS Single Sign OutFilter</filter-name>

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

</filter>

<filter-mapping>

<filter-name>CAS Single Sign OutFilter</filter-name>

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

</filter-mapping>

<listener>

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

</listener>

2.4CAS Server註銷過程

用戶在瀏覽器中點擊「註銷」鏈接,實際瀏覽器會訪問CASServer的註銷頁面。收到註銷請求後,CAS Server會讀取到TGT,並檢查當前用戶登錄過的所有service,並依次發送註銷請求。

2.5CAS 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完整的登錄和註銷過程就完成。

2.6思考:什麼情況統一註銷會失敗

統一註銷的實現,需要CAS Server通過HttpClient訪問CAS Client的service。如果這個訪問過程失敗,就會導致統一註銷失敗。列了幾種情況,不詳述。

1、開發調試階段,使用localhost訪問CAS Client。

2、CAS Server部署在外網,CAS Client部署在內網。

3、網路安全設置,不允許CASServer訪問CAS Client。

3 CAS Client集群的影響

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

3.1Client集群對登錄的影響

假設使用nginx做集群前端,後面部署兩台CAS Client 01的實例。我們看看對登錄過程會有什麼影響。

為了描述方便,CAS Client登錄過程會有三次請求(對應三個過濾器),我們依次命名為Authentication Request / Validation Request / Wrapper Request。

Nginx預設的分發規則,並不是sticky模式,同一個瀏覽器的請求,會按照nginx自身某種規則進行分發。我們曾經測試過,在雙點集群環境下,Authentication Request和ValidationRequest會恰好被分發到兩台伺服器,這就會導致登錄過程死循環。

出現登錄死循環的原因,主要在於nginx分發時,沒有使用sticky策略,也就是同一個瀏覽器的請求,永遠分發給同一台CAS Client實例。預設nginx的分發策略,可以根據用戶IP分發,實現的是同一個IP永遠分發到同一台Client,這樣就能解決死循環的問題。

3.2Client集群對註銷的影響

當nginx實現了sitcky轉發,同一個瀏覽器的訪問會分發到同一個Client1實例,該用戶的會話資訊也一直保存在Client1實例中。

當用戶統一註銷時,由CAS Server向Client發送註銷請求,這時候nginx無法確保按當前用戶進行分發,因此可能會被分發到Client2。這時候,實際效果是註銷失敗。

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

3.2.1 修改nginx分發策略

問題存在的原因,是因為nginx在分發註銷策略時,不能準確分發。如果能在這個環節進行修改,系統程式碼和環境,基本不用做任何修改。

但這個實現難度很大,而且可能會影響nginx的性能。

3.2.2 集群的節點實現Session同步

如果能實現集群Session的同步:同步創建、同步註銷,主要在一個Client上實現了註銷,其他Client也就同步註銷。

這個會對Tomcat性能有影響。

3.2.3 集群節點使用redis保存會話資訊

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

3.2.4 每次請求驗證用戶是否註銷

首先,在CAS Server中實現一個介面,用於判斷某一個ST對應的TGT是否還有效。

在SingleSignOutFilter中,每次訪問都調用CAS Server的這個新介面,判斷用戶是否已經註銷。如果已經註銷,則立刻註銷本實例中的會話資訊。

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