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

 

         CAS 总结之单点退出篇

 

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

 

现将 CASlogout 接口的实现整理如下。

 

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



                                                 CAS logout 功能的序列图

 

 

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

 

我们看一下 CASAbstractWebApplicationServicelogoutOfService 方法的实现。

 

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 方法是调用 serivceoriginUrl 接口,利用 HttpURLConnection 的方式把退出请求发送给 service注意没有给 HttpURLConnection 设置 requestMethod ,因此用的是默认的 GET 方法。 sessionIdentifier 的值是 ST 的值。 serviceresponse 中会解析 logoutRequest 参数中的 sessionIdentifier 的值,然后把 sessionIdentifier 标识的 session kill 掉就可以了。这时我们发现,原理上是可以单点退出的。

 

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

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


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

MANAGED_SESSIONS:keyST 的值, valuesession

ID_TO_SESSION_KEY_MAPPINGkeysessionId,valueST 的值。

 

  • 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 方法,不能单点退出。因为 CASlogoutRequest 请求是用 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:AuthenticationFilterServiceValidationFilterSingleSignOutFilter ,所以三个 Filter 的顺序需要注意,我的顺序为 AuthenticationFilterServiceValidationFilterSingleSignOutFilter ,一开始不行,因为执行退出功能时, CAS 服务端用 HttpURLConnection 访问客户端,没有把 sessionId 代过来,所以在 AuthenticationFilter 中就被 redirectCAS 了,到不了 SingleSignOutFilter ,我做了一个改动,就是在 AuthenticationFilter 中的 redirectUrl ,后面加上了 session ID 的值,格式如:“ ;jsessionid=, 这样 CAS 端解析 service 参数生成 WebApplicationService 时, orginUrl 里就有 sessionId 了。

 

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

 

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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值