CAS 是 Yale 大學發起的一個開源項目,旨在爲 Web 應用系統提供一種可靠的單點登陸方法,CAS 在 2004 年 12 月正式成爲 JA-SIG 的一個項目。CAS 具備如下特色:web
從結構上看,CAS 包含兩個部分: CAS Server 和 CAS Client。CAS Server 須要獨立部署,主要負責對用戶的認證工做;CAS Client 負責處理對客戶端受保護資源的訪問請求,須要登陸時,重定向到 CAS Server。下圖 是 CAS 最基本的協議過程:spring
在圖中第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,接下來的流程就和之前同樣了。cookie
CAS 協議中還提供了 Proxy (代理)模式,以適應更加高級、複雜的應用場景,具體介紹能夠參考 CAS 官方網站上的相關文檔。session
雖然cas是做爲開源單點登陸解決方案的一個不錯選擇,可是官方提供的缺省實現代碼卻不併支持cas server多點部署以及每一個cas client多點部署的狀況。這在現今愈來愈強調服務穩定性的潮流下,多少顯得有些不合時宜。那麼是否是服務是多點部署就不能使用cas了呢?答案是否認的。app
對cas server來講,默認實現不能支持多點部署的緣由在於TGT保存時使用的ticket register類將TGT保存在了Java類的變量中。相關配置以下:memcached
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使用了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協議的單點登陸了。