论坛首页 Java企业应用论坛

【原创】CAS总结之单点退出篇(CAS到底有没有实现单点退出?)

浏览 24643 次
精华帖 (1) :: 良好帖 (11) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2009-12-11   最后修改:2010-01-26

 

         CAS 总结之单点退出篇

 

CAS 到底有没有实现单点退出?本人阅读了 JA-SIG CAS v3.3 ,以及 JA-SIG CAS-CLIENT 3.1.9 的源代码,发现表面上好像实现了单点退出,但实际上却没有真正实现。

 

现将 CAS logout 接口的实现整理如下。

 

首先看一下 CAS logout 功能的序列图。



                                                 CAS logout 功能的序列图

 

 

从图中可以看出, CAS logout 功能有两步,一是调用 TGT 对象中各个 Service logoutOfService 方法,二是在缓存中清除 TGT 对象。

 

我们看一下 CAS AbstractWebApplicationService logoutOfService 方法的实现。

 

public synchronized boolean logOutOfService(final String sessionIdentifier) {
        if (this.loggedOutAlready) {
            return true;
        }

        LOG.debug("Sending logout request for: " + getId());

        final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\""
            + GENERATOR.getNewTicketId("LR")
            + "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime()
            + "\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>"
            + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";
        
        this.loggedOutAlready = true;
        
        if (this.httpClient != null) {
            return this.httpClient.sendMessageToEndPoint(getOriginalUrl(), logoutRequest);
        }
        
        return false;
    }

 

 

    另外 HttpClient 类中 sendMessageToEndPoint 方法的实现如下:

 

public boolean sendMessageToEndPoint(final String url, final String message) {
        HttpURLConnection connection = null;
        BufferedReader in = null;
        try {
            if (log.isDebugEnabled()) {
                log.debug("Attempting to access " + url);
            }
            final URL logoutUrl = new URL(url);
            final String output = "logoutRequest=" + URLEncoder.encode(message, "UTF-8");

            connection = (HttpURLConnection) logoutUrl.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setReadTimeout(this.readTimeout);
            connection.setConnectTimeout(this.connectionTimeout);
            connection.setRequestProperty("Content-Length", ""
                + Integer.toString(output.getBytes().length));
            connection.setRequestProperty("Content-Type",
                "application/x-www-form-urlencoded");
           
            final DataOutputStream printout = new DataOutputStream(connection
                .getOutputStream());
            printout.writeBytes(output);
            printout.flush();
            printout.close();

            in = new BufferedReader(new InputStreamReader(connection
                .getInputStream()));

            while (in.readLine() != null) {
                // nothing to do
            }
            
            if (log.isDebugEnabled()) {
                log.debug("Finished sending message to" + url);
            }
            
            return true;
        } catch (final Exception e) {
            log.error(e,e);
            return false;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (final IOException e) {
                    // can't do anything
                }
            }
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

 

 

通过阅读代码可以发现, logOutOfService 方法是调用 serivce originUrl 接口,利用 HttpURLConnection 的方式把退出请求发送给 service 注意没有给 HttpURLConnection 设置 requestMethod ,因此用的是默认的 GET 方法。 sessionIdentifier 的值是 ST 的值。 service response 中会解析 logoutRequest 参数中的 sessionIdentifier 的值,然后把 sessionIdentifier 标识的 session kill 掉就可以了。这时我们发现,原理上是可以单点退出的。

 

再来看客户端的实现,客户端和单点退出有关的类包括:

  • SingleSignOutFilter :用来解析 logoutRequest 参数。
  • SessionMappingStorage :一个接口,定义了 Session 存储器的方法。  


  • HashMapBackedSessionMappingStorage Session 存储器的实现类,定义了 2 Map 来存储 Session

MANAGED_SESSIONS:key ST 的值, value session

ID_TO_SESSION_KEY_MAPPING key sessionId,value ST 的值。

 

  • SingleSignOutHttpSessionListener :此 Listener 监听到 session destroy 的事件后,用 sessionId 从上述 ID_TO_SESSION_KEY_MAPPING 中取出 ST 的值,然后依据 ST 的值从 MANAGED_SESSIONS 中取出 session, 然后就可以执行其 invalidate 方法了。

 

我们看一下 SingleSignOutFilter 中的 doFilter 方法。

 

public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
       
        if ("POST".equals(request.getMethod())) {
            final String logoutRequest = CommonUtils.safeGetParameter(request, "logoutRequest");

            if (CommonUtils.isNotBlank(logoutRequest)) {

                if (log.isTraceEnabled()) {
                    log.trace ("Logout request=[" + logoutRequest + "]");
                }
                
                final String sessionIdentifier = XmlUtils.getTextForElement(logoutRequest, "SessionIndex");

                if (CommonUtils.isNotBlank(sessionIdentifier)) {
                	final HttpSession session = SESSION_MAPPING_STORAGE.removeSessionByMappingId(sessionIdentifier);

                	if (session != null) {
                        String sessionID = session.getId();

                        if (log.isDebugEnabled()) {
                            log.debug ("Invalidating session [" + sessionID + "] for ST [" + sessionIdentifier + "]");
                        }
                        
                        try {
                        	session.invalidate();
                        } catch (final IllegalStateException e) {
                        	log.debug(e,e);
                        }
                	}
                  return;
                }
            }
        } else {
        	final String artifact = CommonUtils.safeGetParameter(request, this.artifactParameterName);
            final HttpSession session = request.getSession(false);

            if (session != null) {
                if (log.isDebugEnabled()) {
                    log.debug("Storing session identifier for " + session.getId());
                }
                if (CommonUtils.isNotBlank(artifact)) {
                    try {
                        SESSION_MAPPING_STORAGE.removeBySessionById(session.getId());
                    } catch (final Exception e) {
                        // ignore if the session is already marked as invalid.  Nothing we can do!
                    }
                    SESSION_MAPPING_STORAGE.addSessionById(artifact, session);
                }
            } else {
                log.debug("No Session Found, so ignoring.");
            }
        }

        filterChain.doFilter(servletRequest, servletResponse);
}

 

       非常奇怪,这个方法首先判断了 request 的方法,如果是 POST, 则会执行 session.invalidate 方法,从而实现单点退出,如果是 GET ,则只会在存在 ticket 参数的情况下,把 session 存进 SessionMappingStorage ,永远也不执行 session.invalidate 方法,不能单点退出。因为 CAS logoutRequest 请求是用 GET 方法发过来的,所以,单点登录功能没有实现。

 

       本人对 SingleSignOutFilter 中的 doFilter 方法重写了一下,代码如下。经验证,确实实现了单点退出。

 

public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
        
    	final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final String logoutRequest = CommonUtils.safeGetParameter(request, "logoutRequest");
        Enumeration ff = request.getParameterNames();
        String a = request.getQueryString();
        if (CommonUtils.isNotBlank(logoutRequest)) {
        	 final String sessionIdentifier = XmlUtils.getTextForElement(logoutRequest, "SessionIndex");

             if (CommonUtils.isNotBlank(sessionIdentifier)) {
             	final HttpSession session = SESSION_MAPPING_STORAGE.removeSessionByMappingId(sessionIdentifier);

             	if (session != null) {
                     String sessionID = session.getId();                   
                     try {
                     	session.invalidate();
                     } catch (final IllegalStateException e) {
                     	
                     }
             	}
             }
         }
        
        else{
        	final String artifact = CommonUtils.safeGetParameter(request, this.artifactParameterName);
            final HttpSession session = request.getSession(false);
            
            if (CommonUtils.isNotBlank(artifact) && session!=null) {
                try {
                    SESSION_MAPPING_STORAGE.removeBySessionById(session.getId());
                } catch (final Exception e) {
                    
                }
                SESSION_MAPPING_STORAGE.addSessionById(artifact, session);
            }
        }

        filterChain.doFilter(servletRequest, servletResponse);
    }

 

      另外还需要注意的是,因为客户端部署了三个 Filter:AuthenticationFilter ServiceValidationFilter SingleSignOutFilter ,所以三个 Filter 的顺序需要注意,我的顺序为 AuthenticationFilter ServiceValidationFilter SingleSignOutFilter ,一开始不行,因为执行退出功能时, CAS 服务端用 HttpURLConnection 访问客户端,没有把 sessionId 代过来,所以在 AuthenticationFilter 中就被 redirect CAS 了,到不了 SingleSignOutFilter ,我做了一个改动,就是在 AuthenticationFilter 中的 redirectUrl ,后面加上了 session ID 的值,格式如:“ ;jsessionid= , 这样 CAS 端解析 service 参数生成 WebApplicationService 时, orginUrl 里就有 sessionId 了。

 

虽然这样就实现了单点退出,但我感觉 CAS 的这种采用 Filter 的方式太麻烦了,不如让客户应用提供一个 callback url ,CAS 直接调用这个 callback url 来退出更好一些,但这样的话,对 CAS 的改动非常大。

 

     本人博客 :http://zhenkm0507.iteye.com

  • 大小: 14.4 KB
   发表时间:2009-12-12  
大家有没有碰到类似的问题,是如何解决的?
0 请登录后投票
   发表时间:2009-12-13  
我测试CAS3.3.3是实现了单点退出的。过程和你测试的一样,CAS通知各个CLIENT,各个CLIENT里面有filter来接受到SAML2.0的请求,然后实现了退出。不过我遇到的问题是,我是采用loadbalance来配置的各个client和CAS,所以登出的时候这个SAML2.0已经不知道把退出的请求发给loadbalance后面的哪个client了,这里又不是IE,而是server向各个地方发SAML2的请求,所以也不能session stick,所以我是采用比较土的办法,改写了退出的地方,让它一个一个去调用各个client的退出。我的client是用的spring security2.0。
CAS的论坛上倒是在说CAS4.0出来以后要实现真正的单点退出,解决这种集群里面的问题,不光是考虑CAS认证中心的集群,还要考虑到各个CLIENT也可能是个集群。不过这个可能就不知道啥时候去了,我一直关心CAS的站点,都没有看到要出4.0.。。。。
0 请登录后投票
   发表时间:2009-12-15  
多谢sillycat回复啊,谢谢你提醒了我单点退出时还要考虑集群的问题!
0 请登录后投票
   发表时间:2009-12-30  
只能说你没有研究透彻源码。。。。。
0 请登录后投票
   发表时间:2009-12-30  
AuthenticationFilter 、 ServiceValidationFilter 、 SingleSignOutFilter

顺序都搞错了。。SingleSignOutFilter  要放在最前面
0 请登录后投票
   发表时间:2009-12-30  
非常感谢kevindurant关注!!不过在我的实现里面,SingleSignOutFilter 放在最后也是可以的,前提是做过更改。没必要说顺序都搞错了,ok?
0 请登录后投票
   发表时间:2010-02-09  
<filter>
      <!-- CAS 登出-->
         <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>/logout</url-pattern>
</filter-mapping>

这样配置拦截器 无效 是什么原因?
0 请登录后投票
   发表时间:2010-03-02  
估计是忘记写了,呵呵,我在3.3.5中看到的代码是有的,如下:

URL logoutUrl = new URL(url);
            String output = (new StringBuilder()).append("logoutRequest=").append(URLEncoder.encode(message, "UTF-8")).toString();
            connection = (HttpURLConnection)logoutUrl.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.setReadTimeout(readTimeout);
            connection.setConnectTimeout(connectionTimeout);
            connection.setRequestProperty("Content-Length", Integer.toString(output.getBytes().length));
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            DataOutputStream printout = new DataOutputStream(connection.getOutputStream());
            printout.writeBytes(output);
            printout.flush();
            printout.close();
0 请登录后投票
   发表时间:2010-06-21  
zhenkm0507 写道
非常感谢kevindurant关注!!不过在我的实现里面,SingleSignOutFilter 放在最后也是可以的,前提是做过更改。没必要说顺序都搞错了,ok?


默认配置是sign out的放在最前面的,因为这样就可以避免被Authentication Filter拦截了。

不过我用了一个笨方法。。。
自己写了个filter,当访问项目的/logout地址的时候直接清除Cookie和Session。

不过这样项目多了就郁闷了。。。

还是期待CAS4吧,我在他们的SVN上看到应该差不多完成了。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics