0
点赞
收藏
分享

微信扫一扫

源代码解读Cas实现单点登出(single sign out)功能实现原理


关于Cas实现单点登入(single sing on)功能的文章在网上介绍的比较多,想必大家多多少少都已经有所了解,在此就不再做具体介绍。如果不清楚的,那只能等我把single sign on这块整理出来后再了解了。当然去cas官方网站也是有很多的文章进行介绍。cas官网http://www.ja-sig.org/products/cas/。


ok,现在开始本文的重点内容讲解,先来了解一下cas 实现single sign out的原理,如图所示:

源代码解读Cas实现单点登出(single sign out)功能实现原理_web.xml



                                        图一

源代码解读Cas实现单点登出(single sign out)功能实现原理_应用服务器_02


                                    图二


第一张图演示了单点登陆的工作原理。

第二张图演示了单点登出的工作原理。


从第一张图中,当一个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,内容格式如下:



< 
 saml:NameID 
 > 
 @NOT_USED@ 
 </ 
 saml:NameID 
 > 
 
  < 
 samlp:SessionIndex 
 > 
 [SESSION IDENTIFIER] 
 </ 
 samlp:SessionIndex 
 >

所有收到请求的应用服务器application会解析这个参数,取得sessionId,根据这个Id取得session后,把session删除。


这样就实现单点登出的功能。



知道原理后,下面是结合源代码来讲述一下内部的代码怎么实现的。


首先,要实现single sign out在 应用服务器application端的web.xml要加入以下配置


< 
 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 
 >



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



配置部分的目的是在CAS server回调所有的application进行单点登出操作的时候,需要这个filter来实现session清楚。



主要代码如下:


org.jasig.cas.client.session.SingleSignOutFilter


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销毁事件


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文件中

< 
 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配置文件中。


   

< 
 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" 
   
 />


那CentralAuthenticationServiceImpl是怎么调用的呢?


我们跟踪一下代码,在创建ticket的方法 public String createTicketGrantingTicket(final Credentials credentials)中


可以找到以下这样一段代码:


      

// 
 创建 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销毁的时候,会做什么


事情,代码如下:

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方法做什么事情

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的真正实现代码,下面是主要代码片段:


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!


举报

相关推荐

0 条评论