分享

cas+shiro单点登出的坑 | Server Not Down

 KILLKISS 2017-04-13

情景

cas server:cas
client server: C1
client server: C2

当用户在C1和C2都登录之后,获取到改用户在两个系统内各自需要的权限之后,在C1做登出操作,按照网上大部分的配置方法(web.xml中增加SingleSignOutFilter和SingleSignOutHttpSessionListener),可以在效果上看起来是登出了,但是并没有完全登出。

即:
C1和C2的JSESSIONID对应在服务器的session被销毁,浏览器两个JSESSIONID失效(看起来登出了)
cas的cookie(TGT)失效
C1服务器上,对应的用户权限清除(C1是完全退出了)
C2服务器上,对应的用户权限没有清除(没完全退出)

原理分析

Created with Rapha?l 2.1.2BrowserBrowserC1C1cascasC2C2logout request(1)C1 subject.logout(), redirect to cas (2)cas logout path(3)notify C2 the user had logout(4)

1,2,3都很正常,问题出在第四步。

第四步仅仅是被SingleSignOutFilter拦截,根据service-ticket销毁掉改用户对应的session,而并没有调用shiro的subject.logout, 显然,subject.logout是做了销毁权限缓存等操作的

这样就会导致最终C2上的用户权限没有被清除,若在此时用户权限被修改,就会导致即使登出,C2上的权限也没有刷新

解决方案

方案一

权限缓存是可以设置过期时间的,那么简单点,只要给权限缓存加上过期时间即可,这样如果权限被修改,即使用户不登出,在过期之后,权限也会被刷新

方案二

http://howiefh./2015/05/19/shiro-cas-single-sign-on/ 有一个很详细的说明,但是没仔细看,简单的说就是使用ServletContainerSessionManager,即shiro自己的session管理,似乎可以解决问题,但是未验证

方案三

思路很简单,重写SingleSignOutFilter, 在登出的时候,调用subject.logout 即可。

奈何太年轻,这种方案有很多坑

坑一

问题:

Subject是由session中存放的一个key生成的,但是时序图中第四步是有cas发起的请求,而不是用户浏览器,即这个session中没有Subject信息,shiro无法获取到具体信息。

解决:
SingleSignOutFilter中有存储一份 service-ticket与session的映射关系,那么只要在第四步中 利用 service-ticket取到session,再从session中取到SimplePrincipalCollection信息放入subject即可

坑二

问题:

subject不提供设置principal接口,service-ticket session映射关系未提供get接口

解决:
反射搞定,但是总觉得不靠谱呢。。

坑三

问题:

SingleSignOutFilter是在ShiroFilter chain之前,也就是说,如果重写SingleSignOutFilter,在里边连一个不包含Principal的Subject都获取不到,但是如果把这个SimplePrincipalCollection放到 shrioFilter之后,登录的时候又会有问题
这是一个鸡生蛋和蛋生鸡的问题啊。。。

解决:
问题总是能解决的,放在前边后边都不行,那么放一起吧。对,把SingleSignOutFilter放到ShiroFilter之中, 原以为ShiroFilter会对符合过滤规则的做一个filter chain,结果并不是。

shiro会针对配置的filter规则,取第一个匹配的作为最终的filter,而后边符合规则的就会被忽略掉

所以这里,要把SingleSignOutFilter和Shiro自己提供的CasFilter合并起来,放在一起作为一个filter

方案三代码

经过这么一折腾,于是就有了下面的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public void doFilterInternal(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
if (handler.isTokenRequest(request)) {
handler.recordSession(request); // 登录,记录session SingleSignOutFilter做的事情
super.doFilterInternal(servletRequest, servletResponse, filterChain); // 记录完了之后,就调用CasFilter自己的doFilterInternal
return;
} else if (handler.isLogoutRequest(request)) { // 如果是登出
// 一堆的代码,就是为了获取SimplePrincipalCollection,设置到Subject里边去,并在最后调用subject.logout()
final String logoutMessage = CommonUtils.safeGetParameter(request, "logoutRequest");
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
if (CommonUtils.isNotBlank(token)) {
HttpSession session = null;
try {
Field msField = handler.getSessionMappingStorage().getClass().getDeclaredField("MANAGED_SESSIONS");
msField.setAccessible(true);
Map<String,HttpSession> MANAGED_SESSIONS = (Map)msField.get(handler.getSessionMappingStorage());
session = MANAGED_SESSIONS.get(token);
} catch (Exception e) {
}
if (session != null) {
Subject subject = getSubject(servletRequest, servletResponse);
ShiroUser shiroUser = (ShiroUser)(((SimplePrincipalCollection)(session.getAttribute("org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY"))).getPrimaryPrincipal());
SimplePrincipalCollection pc = new SimplePrincipalCollection(shiroUser, shiroUser.getName());
try {
Field principalsField = subject.getClass().getSuperclass().getDeclaredField("principals");
principalsField.setAccessible(true);
principalsField.set(subject, pc);
} catch (Exception e) {
}
try {
subject.logout();
} catch (SessionException ise) {
}
}
}
// logout之后,还要销毁session SingleSignOutFilter做的事情
handler.destroySession(request);
return;
} else {
log.trace("Ignoring URI " + request.getRequestURI());
}
filterChain.doFilter(servletRequest, servletResponse);
}

代码逻辑很简单,主要是要找到这么个解决方案,得一点点的调试和摸索,也是蛮有意思。
另外web.xml中的SingleSignOutFilter需要去掉,因为我们已经移到Shiro里边了,但是Listener需要保留,并且需要自己重写(里边有调用SingleSignOutFilter的方法,需要改掉), 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<bean id="casFilter" class="com.simpletour.sso.shiro.STSingleSignOutFilter">
<property name="failureUrl" value="${sso.cas.client}${sso.cas.client.home}"/>
</bean>
<bean id="shiroFilter" class="com.simpletour.sso.shiro.STShiroFilterFactoryBean" init-method="init">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="${sso.cas.server}?service=${sso.cas.client}/cas/login" />
<property name="successUrl" value="${sso.cas.client.home}" />
<property name="filters">
<map>
<entry key="cas" value-ref="casFilter"/>
<entry key="logout" value-ref="logoutFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<value>
/static/** = anon
/config_* = anon
/cas/* = cas <!--这里,cas/login, cas/logout 都走我们刚刚写的filter-->
/logout = logout
/** = user
</value>
</property>
</bean>

最后

准备找个时间写个cas的faq,毕竟在开发过程中,遇到的很多常见问题,很是烦躁。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多