posts - 156,  comments - 601,  trackbacks - 0

          聲明:本博客屬作者原創,如果任何網站轉載本文,請注明作者及引用來源,謝謝合作!

          關于Cas實現單點登入(single sing on)功能的文章在網上介紹的比較多,想必大家多多少少都已經有所了解,在此就不再做具體介紹。如果不清楚的,那只能等我把single sign on這塊整理出來后再了解了。當然去cas官方網站也是有很多的文章進行介紹。cas官網http://www.ja-sig.org/products/cas/

          ok,現在開始本文的重點內容講解,先來了解一下cas 實現single sign out的原理,如圖所示:



                                                  圖一


                                              圖二

          第一張圖演示了單點登陸的工作原理。
          第二張圖演示了單點登出的工作原理。

          從第一張圖中,當一個web瀏覽器登錄到應用服務器時,應用服務器(application)會檢測用戶的session,如果沒有session,則應用服務器會把url跳轉到CAS server上,要求用戶登錄,用戶登錄成功后,CAS server會記請求的application的url和該用戶的sessionId(在應用服務器跳轉url時,通過參數傳給CAS server)。此時在CAS服務器會種下TGC Cookie值到webbrowser.擁有該TGC Cookie的webbrowser可以無需登錄進入所有建立sso服務的應用服務器application。

          在第二張圖中,當一個web瀏覽器要求登退應用服務器,應用服務器(application)會把url跳轉到CAS server上的 /cas/logout url資源上,

          CAS server接受請求后,會檢測用戶的TCG Cookie,把對應的session清除,同時會找到所有通過該TGC sso登錄的應用服務器URL提交請求,所有的回調請求中,包含一個參數logoutRequest,內容格式如下:

          <samlp:LogoutRequest ID="[RANDOM ID]" Version="2.0" IssueInstant="[CURRENT DATE/TIME]">
          <saml:NameID>@NOT_USED@</saml:NameID>
          <samlp:SessionIndex>[SESSION IDENTIFIER]</samlp:SessionIndex>
          </samlp:LogoutRequest>


          所有收到請求的應用服務器application會解析這個參數,取得sessionId,根據這個Id取得session后,把session刪除。
          這樣就實現單點登出的功能。

          知道原理后,下面是結合源代碼來講述一下內部的代碼怎么實現的。
          首先,要實現single sign out在 應用服務器application端的web.xml要加入以下配置
          <filter>
             
          <filter-name>CAS Single Sign Out Filter</filter-name>
             
          <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
          </filter>

          <filter-mapping>
             
          <filter-name>CAS Single Sign Out Filter</filter-name>
             
          <url-pattern>/*</url-pattern>
          </filter-mapping>

          <listener>
              
          <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
          </listener>

          注:如果有配置CAS client Filter,則CAS Single Sign Out Filter 必須要放到CAS client Filter之前。

          配置部分的目的是在CAS server回調所有的application進行單點登出操作的時候,需要這個filter來實現session清楚。

          主要代碼如下:
          org.jasig.cas.client.session.SingleSignOutFilter
           1 public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain      
           2 
           3 filterChain) throws IOException, ServletException {
           4         final HttpServletRequest request = (HttpServletRequest) servletRequest;
           5 
           6         if ("POST".equals(request.getMethod())) {
           7             final String logoutRequest = request.getParameter("logoutRequest");
           8 
           9             if (CommonUtils.isNotBlank(logoutRequest)) {
          10 
          11                 if (log.isTraceEnabled()) {
          12                     log.trace ("Logout request=[" + logoutRequest + "]");
          13                 }
          14                 //從xml中解析 SessionIndex key值
          15                 final String sessionIdentifier = XmlUtils.getTextForElement(logoutRequest, "SessionIndex");
          16 
          17                 if (CommonUtils.isNotBlank(sessionIdentifier)) {
          18                         //根據sessionId取得session對象
          19                     final HttpSession session = SESSION_MAPPING_STORAGE.removeSessionByMappingId(sessionIdentifier);
          20 
          21                     if (session != null) {
          22                         String sessionID = session.getId();
          23 
          24                         if (log.isDebugEnabled()) {
          25                             log.debug ("Invalidating session [" + sessionID + "] for ST [" + sessionIdentifier + "]");
          26                         }
          27                         
          28                         try {
          29                 //讓session失效
          30                             session.invalidate();
          31                         } catch (final IllegalStateException e) {
          32                             log.debug(e,e);
          33                         }
          34                     }
          35                   return;
          36                 }
          37             }
          38         } else {//get方式 表示登錄,把session對象放到SESSION_MAPPING_STORAGE(map對象中)
          39             final String artifact = request.getParameter(this.artifactParameterName);
          40             final HttpSession session = request.getSession();
          41             
          42             if (log.isDebugEnabled() && session != null) {
          43                 log.debug("Storing session identifier for " + session.getId());
          44             }
          45             if (CommonUtils.isNotBlank(artifact)) {
          46                 SESSION_MAPPING_STORAGE.addSessionById(artifact, session);
          47             }
          48         }
          49 
          50         filterChain.doFilter(servletRequest, servletResponse);
          51     }

          SingleSignOutHttpSessionListener實現了javax.servlet.http.HttpSessionListener接口,用于監聽session銷毀事件
           1 public final class SingleSignOutHttpSessionListener implements HttpSessionListener {
           2 
           3     private Log log = LogFactory.getLog(getClass());
           4 
           5     private SessionMappingStorage SESSION_MAPPING_STORAGE;
           6     
           7     public void sessionCreated(final HttpSessionEvent event) {
           8         // nothing to do at the moment
           9     }
          10 
          11     //session銷毀時
          12     public void sessionDestroyed(final HttpSessionEvent event) {
          13         if (SESSION_MAPPING_STORAGE == null) {//如果為空,創建一個sessionMappingStorage 對象
          14             SESSION_MAPPING_STORAGE = getSessionMappingStorage();
          15         }
          16         final HttpSession session = event.getSession();//取得當然要銷毀的session對象
          17         
          18         if (log.isDebugEnabled()) {
          19             log.debug("Removing HttpSession: " + session.getId());
          20         }
          21         //從SESSION_MAPPING_STORAGE map根據sessionId移去session對象
          22         SESSION_MAPPING_STORAGE.removeBySessionById(session.getId());
          23     }
          24 
          25     /**
          26      * Obtains a {@link SessionMappingStorage} object. Assumes this method will always return the same
          27      * instance of the object.  It assumes this because it generally lazily calls the method.
          28      * 
          29      * @return the SessionMappingStorage
          30      */
          31     protected static SessionMappingStorage getSessionMappingStorage() {
          32         return SingleSignOutFilter.getSessionMappingStorage();
          33     }
          34 }

          接下來,我們來看一下CAS server端回調是怎么實現的
          先來看一下配置,我們知道CAS server所有的用戶登錄,登出操作,都是由CentralAuthenticationServiceImpl對象來管理。
          我們就先把到CentralAuthenticationServiceImpl的spring配置,在applicationContext.xml文件中
          <!-- CentralAuthenticationService -->
              
          <bean id="centralAuthenticationService" class="org.jasig.cas.CentralAuthenticationServiceImpl"
                  p:ticketGrantingTicketExpirationPolicy-ref
          ="grantingTicketExpirationPolicy"
                  p:serviceTicketExpirationPolicy-ref
          ="serviceTicketExpirationPolicy"
                  p:authenticationManager-ref
          ="authenticationManager"
                  p:ticketGrantingTicketUniqueTicketIdGenerator-ref
          ="ticketGrantingTicketUniqueIdGenerator"
                  p:ticketRegistry-ref
          ="ticketRegistry"
                      p:servicesManager-ref
          ="servicesManager"
                      p:persistentIdGenerator-ref
          ="persistentIdGenerator"
                  p:uniqueTicketIdGeneratorsForService-ref
          ="uniqueIdGeneratorsMap" />

          配置使用了spring2.0的xsd。CentralAuthenticationServiceImpl有一個屬性叫uniqueTicketIdGeneratorsForService,它是一個map對象
          它的key值是所有實現org.jasig.cas.authentication.principal.Service接口的類名,用于保存Principal對象和進行單點登出回調

          application server時使用 value值為org.jasig.cas.util.DefaultUniqueTicketIdGenerator對象,用于生成唯一的TGC ticket。
          該屬性引用的uniqueIdGeneratorsMap bean在uniqueIdGenerators.xml配置文件中。
          <util:map id="uniqueIdGeneratorsMap">
                  
          <entry
                      
          key="org.jasig.cas.authentication.principal.SimpleWebApplicationServiceImpl"
                      value-ref
          ="serviceTicketUniqueIdGenerator" />
                  
          <entry
                      
          key="org.jasig.cas.support.openid.authentication.principal.OpenIdService"
                      value-ref
          ="serviceTicketUniqueIdGenerator" />
                  
          <entry
                      
          key="org.jasig.cas.authentication.principal.SamlService"
                      value-ref
          ="samlServiceTicketUniqueIdGenerator" />
                  
          <entry
                      
          key="org.jasig.cas.authentication.principal.GoogleAccountsService"
                      value-ref
          ="serviceTicketUniqueIdGenerator" />
              
          </util:map>

          那CentralAuthenticationServiceImpl是怎么調用的呢?
          我們跟蹤一下代碼,在創建ticket的方法 public String createTicketGrantingTicket(final Credentials credentials)中
          可以找到以下這樣一段代碼:
          1         //創建 TicketGrantingTicketImpl 實例
          2             final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
          3                 this.ticketGrantingTicketUniqueTicketIdGenerator
          4                     .getNewTicketId(TicketGrantingTicket.PREFIX),
          5                 authentication, this.ticketGrantingTicketExpirationPolicy);
          6         //并把該對象保存到 ticketRegistry中
          7         this.ticketRegistry.addTicket(ticketGrantingTicket);

          上面的代碼,看到ticketRegistry對象保存了創建的TicketGrantingTicketImpl對象,下面我們看一下當ticket銷毀的時候,會做什么
          事情,代碼如下:
           1     public void destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
           2         Assert.notNull(ticketGrantingTicketId);
           3 
           4         if (log.isDebugEnabled()) {
           5             log.debug("Removing ticket [" + ticketGrantingTicketId
           6                 + "] from registry.");
           7         }
           8     //從 ticketRegistry對象中,取得TicketGrantingTicket對象
           9         final TicketGrantingTicket ticket = (TicketGrantingTicket) this.ticketRegistry
          10             .getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
          11 
          12         if (ticket == null) {
          13             return;
          14         }
          15 
          16         if (log.isDebugEnabled()) {
          17             log.debug("Ticket found.  Expiring and then deleting.");
          18         }
          19         ticket.expire();//調用expire()方法,讓ticket過期失效
          20         this.ticketRegistry.deleteTicket(ticketGrantingTicketId);//從ticketRegistry中刪除的ticket 對象
          21     }

          我們看到,它是從ticketRegistry對象中取得TicketGrantingTicket對象后,調用expire方法。接下來,要關心的就是expire方法做什么事情
           1     public synchronized void expire() {
           2         this.expired.set(true);
           3         logOutOfServices();
           4     }
           5 
           6     private void logOutOfServices() {
           7         for (final Entry<String, Service> entry : this.services.entrySet()) {
           8             entry.getValue().logOutOfService(entry.getKey());
           9         }
          10     }

          從代碼可以看到,它是遍歷每個 Service對象,并執行logOutOfService方法,參數是String sessionIdentifier
          現在我們可以對應中,它存放的Service就是在uniqueIdGeneratorsMap bean定義中的那些實現類

          因為logOutOfService方法的實現,所有實現類都是由它們繼承的抽象類AbstractWebApplicationService來實現,我們來看一下
          AbstractWebApplicationService的logOutOfService方法,就可以最終找出,實現single sign out的真正實現代碼,下面是主要代碼片段:

           1   public synchronized boolean logOutOfService(final String sessionIdentifier) {
           2         if (this.loggedOutAlready) {
           3             return true;
           4         }
           5 
           6         LOG.debug("Sending logout request for: " + getId());
           7         //組裝 logoutRequest參數內容
           8         final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\""
           9             + GENERATOR.getNewTicketId("LR")
          10             + "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime()
          11             + "\"><saml:NameID 
          12 
          13 xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>"
          14             + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";
          15         
          16         this.loggedOutAlready = true;
          17         //回調所有的application,getOriginalUrl()是取得回調的application url
          18         if (this.httpClient != null) {
          19             return this.httpClient.sendMessageToEndPoint(getOriginalUrl(), logoutRequest);
          20         }
          21         
          22         return false;
          23     }

          至此,已經通過源代碼把 CAS實現 single sign out的實現原理和方法完整敘述了一遍,希望對CAS感興趣的朋友有所幫忙,
          如果有什么問題也希望大家提出和指正。

          Good Luck!
          Yours Matthew!

          posted on 2008-07-09 22:44 x.matthew 閱讀(31792) 評論(13)  編輯  收藏 所屬分類: Spring|Hibernate|Other framework
          主站蜘蛛池模板: 沂水县| 屏山县| 武定县| 淮阳县| 随州市| 五大连池市| 青阳县| 安阳县| 灵台县| 砀山县| 万盛区| 武功县| 怀化市| 县级市| 年辖:市辖区| 台东市| 黄大仙区| 沅江市| 若羌县| 岗巴县| 花莲市| 电白县| 揭阳市| 霍邱县| 石河子市| 涪陵区| 株洲县| 广水市| 论坛| 炉霍县| 黄冈市| 保康县| 廊坊市| 新龙县| 德化县| 华宁县| 长治市| 雅安市| 呼图壁县| 保定市| 文成县|