CAS 介紹
CAS 是 Yale 大學發起的一個開源項目,旨在為 Web 應用系統提供一種可靠的單點登錄方法,CAS 在 2004 年 12 月正式成為 JA-SIG 的一個項目。CAS 具有以下特點:
- 開源的企業級單點登錄解決方案。
- CAS Server 為需要獨立部署的 Web 應用。
- CAS Client 支持非常多的客戶端(這里指單點登錄系統中的各個 Web 應用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。
CAS 原理和協議
從結構上看,CAS 包含兩個部分: CAS Server 和 CAS Client。CAS Server 需要獨立部署,主要負責對用戶的認證工作;CAS Client 負責處理對客戶端受保護資源的訪問請求,需要登錄時,重定向到 CAS Server。下圖 是 CAS 最基本的協議過程:

在圖中第3步用戶認證成功后,cas server會生成Ticket Granting Ticket(票據授權票據,簡稱TGT),同時將TGT值以CASTGC為名保存到瀏覽器的cookie中,之后生成Service Ticket(服務票據,簡稱ST)並緩存,在第4步時將ST通過瀏覽器重定向的URL傳給cas client。
當cas client驗證ST時,是在后台請求cas server驗證ST,而cas server在已緩存的ST中查找是否存在cas client傳來的ST,若存在則返回驗證成功同時將該ST刪除(這就保證了用同一ST不能反復進入client應用,同時這也是為什么不直接將TGT返回給cas client的原因)。
那么名為CASTGC的cookie起什么作用呢?
當客戶訪問另一個cas client時,同樣會被重定向到cas server,而此時我們並不希望再次讓用戶輸入用戶密碼登陸,名為CASTGC的cookie這時就體現出作用來了,cas server發現存在名為CASTGC的cookie就將其值在已保存的TGT中查找,若存在,則說明已存在合法的TGT,cas server就根據該TGT生成新的ST,接下來的流程就和以前一樣了。
CAS 協議中還提供了 Proxy (代理)模式,以適應更加高級、復雜的應用場景,具體介紹可以參考 CAS 官方網站上的相關文檔。
在多點環境下使用CAS
雖然cas是作為開源單點登錄解決方案的一個不錯選擇,但是官方提供的缺省實現代碼卻不並支持cas server多點部署以及每個cas client多點部署的情況。這在現今越來越強調服務穩定性的潮流下,多少顯得有些不合時宜。那么是不是服務是多點部署就不能使用cas了呢?答案是否定的。
多點部署cas server
對cas server來說,默認實現不能支持多點部署的原因在於TGT保存時使用的ticket register類將TGT保存在了Java類的變量中。相關配置如下:
WEB-INF\spring-configuration\ticketRegistry.xml:
<bean id="ticketRegistry" class="org.jasig.cas.ticket.registry.DefaultTicketRegistry" />
如果要支持多點部署,我們可以通過引入memcached的方式,在多點環境下仍然能夠正常使用cas。我們可以新創建一個類(如MemcachedTicketRegistry)實現AbstractTicketRegistry接口,修改相關配置如下:
WEB-INF\spring-configuration\ticketRegistry.xml:(在此省略了memcachedClient的配置)
<bean id="ticketRegistry" class="com.xxx.cas.server.MemcachedTicketRegistry"> <property name="client" ref="memcachedClient" /> </bean>
這樣cas server已經可以多點部署了,然而此時我們會發現單點登出功能不正常了。通過debug和查看代碼,發現TGT中保存的service集合為空,這是單點登出不正常的直接原因,因為cas server會遍歷TGT中保存的service集合,依次向對應的cas client發出退出請求。然而為什么TGT中保存的service集合會為空呢?這是因為TGT從第一次被保存到memcached后就再也沒有被保存到memcached,這樣從memcached中取得的TGT自然還是最初的TGT,當然其中的service會為空了,而cas默認實現中TGT是始終保持在內存中的自然不會有問題。既然找到了問題的原因就簡單了,我們只要每當TGT增加service后,再次將TGT保存到memcached就能解決這個問題。
WEB-INF\spring-configuration\applicationContext.xml修改如下:
<bean id="centralAuthenticationService" class="org.jasig.cas.CentralAuthenticationServiceImpl" ... /> 替換為 <bean id="centralAuthenticationService" class="com.xxx.cas.server.CentralAuthenticationServiceImpl" ... />
com.xxx.cas.server.CentralAuthenticationServiceImpl類相比org.jasig.cas.CentralAuthenticationServiceImpl類有如下不同:
this.serviceTicketRegistry.addTicket(serviceTicket); //com.xxx.cas.server.CentralAuthenticationServiceImp類增加了下面一行: this.serviceTicketRegistry.addTicket(ticketGrantingTicket);
多點部署cas client
對cas client來說,默認實現不能支持多點部署的原因在於cas client使用了session來保存憑證,要知道多點部署的應用其session是各自獨立的,即使通過配置實現了session的同步,性能也會很差。那么如何解決這個問題呢?答案還是memcached。
我們可以用過濾器filter將自定義的request傳遞給后面的調用,filter內容如下:
SnaHttpServletRequest request = new SnaHttpServletRequest((HttpServletRequest) req,(HttpServletResponse) res, client); WebContext instance = new WebContext(request, (HttpServletResponse) res, ctx); try { WebContext.set(instance); chain.doFilter(request, res); } finally { request.save(); WebContext.set(null); }
其中SnaHttpServletRequest類繼承自HttpServletRequestWrapper類,覆蓋HttpSession getSession()和HttpSession getSession(boolean create)方法,使這兩個方法返回實現HttpSession接口但是以memcached為核心實現的類(如MemcachedSession),而MemcachedSession類必定是以cookie為依據的,否則無法返回正確的數據。
這其中代碼的具體實現,網上已經有不少,這里就不展示了。
cas client經過這樣的改動后,在多點部署下可以單點登陸了,但單點登出仍有問題。原因是因為SingleSignOutFilter將ticketId和session對應關系用HashMap來保存,在多點環境下自然不能正常工作。我們可以將ticketId和cookie值的對應關系保存在memcached上,從而實現單點登出。修改辦法如下:
SingleSignOutFilter類:
public void init(final FilterConfig filterConfig) throws ServletException { String memcachedId = "memcachedClient"; this.client = (XMemcachedClient) WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext()).getBean(memcachedId); handler.setSessionMappingStorage(new MemcachedBackedSessionMappingStorage(client));
//紅色部分是新增的內容 if (!isIgnoreInitConfiguration()) { handler.setArtifactParameterName(getPropertyFromInitParams(filterConfig, "artifactParameterName", "ticket")); handler.setLogoutParameterName(getPropertyFromInitParams(filterConfig, "logoutParameterName", "logoutRequest")); } handler.init(); }
MemcachedBackedSessionMappingStorage類繼承自SessionMappingStorage接口,代碼如下:
public final class MemcachedBackedSessionMappingStorage implements SessionMappingStorage { private XMemcachedClient client; private static final int TIMEOUT = 60 * 60 * 24; private final String MANAGED_SESSIONS = "MANAGED_SESSIONS."; private final String ID_TO_SESSION_KEY_MAPPING = "ID_TO_SESSION_KEY_MAPPING."; private final Log log = LogFactory.getLog(getClass()); public MemcachedBackedSessionMappingStorage(XMemcachedClient client) { this.client = client; } public synchronized void addSessionById(String mappingId, HttpSession session) { try { client.set(ID_TO_SESSION_KEY_MAPPING + session.getId(), TIMEOUT, mappingId); client.set(MANAGED_SESSIONS + mappingId, TIMEOUT, session.getId()); } catch (Exception e) { throw new RuntimeException(e); } } public synchronized void removeBySessionById(String sessionId) { if (log.isDebugEnabled()) { log.debug("Attempting to remove Session=[" + sessionId + "]"); } try { final String key = client.get(ID_TO_SESSION_KEY_MAPPING + sessionId); if (log.isDebugEnabled()) { if (key != null) { log.debug("Found mapping for session. Session Removed."); } else { log.debug("No mapping for session found. Ignoring."); } } client.delete(MANAGED_SESSIONS + key); client.delete(ID_TO_SESSION_KEY_MAPPING + sessionId); } catch (Exception e) { throw new RuntimeException(e); } } public synchronized HttpSession removeSessionByMappingId(String mappingId) { HttpSession session = null; try { String sessionId = client.get(MANAGED_SESSIONS + mappingId); session = new MemcachedSession(client, sessionId, false); } catch (Exception e) { throw new RuntimeException(e); } if (session != null) { removeBySessionById(session.getId()); } return session; } }
其它修改
在CAS的默認實現中,所有與 CAS 的交互均采用 SSL 協議,確保ST 和 TGC(Ticket Granted Cookie,對應TGT的名為CASTGC的cookie) 的安全性。這雖然極大保證了安全性,但在某些情況下,並不想使用SSL協議,那么可以進行如下修改:
WEB-INF\spring-configuration\ticketGrantingTicketCookieGenerator.xml:
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="true" p:cookieMaxAge="-1" p:cookieName="CASTGC" p:cookiePath="/cas" /> 修改為: <bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="false" p:cookieMaxAge="-1" p:cookieName="CASTGC" p:cookiePath="/cas" />
WEB-INF\spring-configuration\warnCookieGenerator.xml:
<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="true" p:cookieMaxAge="-1" p:cookieName="CASPRIVACY" p:cookiePath="/cas" /> 修改為: <bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="false" p:cookieMaxAge="-1" p:cookieName="CASPRIVACY" p:cookiePath="/cas" />
WEB-INF\deployerConfigContext.xml:
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" /> 修改為: <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" p:requireSecure="false"/>
進行上面的修改后cas就能支持普通http協議的單點登錄了。
