精华帖 (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 功能有两步,一是调用 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 掉就可以了。这时我们发现,原理上是可以单点退出的。
再来看客户端的实现,客户端和单点退出有关的类包括:
MANAGED_SESSIONS:key 为 ST 的值, value 为 session ; ID_TO_SESSION_KEY_MAPPING : key 为 sessionId,value 为 ST 的值。
我们看一下 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 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2009-12-12
大家有没有碰到类似的问题,是如何解决的?
|
|
返回顶楼 | |
发表时间: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.。。。。 |
|
返回顶楼 | |
发表时间:2009-12-15
多谢sillycat回复啊,谢谢你提醒了我单点退出时还要考虑集群的问题!
|
|
返回顶楼 | |
发表时间:2009-12-30
只能说你没有研究透彻源码。。。。。
|
|
返回顶楼 | |
发表时间:2009-12-30
AuthenticationFilter 、 ServiceValidationFilter 、 SingleSignOutFilter
顺序都搞错了。。SingleSignOutFilter 要放在最前面 |
|
返回顶楼 | |
发表时间:2009-12-30
非常感谢kevindurant关注!!不过在我的实现里面,SingleSignOutFilter 放在最后也是可以的,前提是做过更改。没必要说顺序都搞错了,ok?
|
|
返回顶楼 | |
发表时间: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> 这样配置拦截器 无效 是什么原因? |
|
返回顶楼 | |
发表时间: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(); |
|
返回顶楼 | |
发表时间:2010-06-21
zhenkm0507 写道 非常感谢kevindurant关注!!不过在我的实现里面,SingleSignOutFilter 放在最后也是可以的,前提是做过更改。没必要说顺序都搞错了,ok? 默认配置是sign out的放在最前面的,因为这样就可以避免被Authentication Filter拦截了。 不过我用了一个笨方法。。。 自己写了个filter,当访问项目的/logout地址的时候直接清除Cookie和Session。 不过这样项目多了就郁闷了。。。 还是期待CAS4吧,我在他们的SVN上看到应该差不多完成了。 |
|
返回顶楼 | |