情景cas server:cas client server: C1 client server: C2
當(dāng)用戶在C1和C2都登錄之后,獲取到改用戶在兩個(gè)系統(tǒng)內(nèi)各自需要的權(quán)限之后,在C1做登出操作,按照網(wǎng)上大部分的配置方法(web.xml中增加SingleSignOutFilter和SingleSignOutHttpSessionListener),可以在效果上看起來(lái)是登出了,但是并沒(méi)有完全登出。
即: C1和C2的JSESSIONID對(duì)應(yīng)在服務(wù)器的session被銷(xiāo)毀,瀏覽器兩個(gè)JSESSIONID失效(看起來(lái)登出了) cas的cookie(TGT)失效 C1服務(wù)器上,對(duì)應(yīng)的用戶權(quán)限清除(C1是完全退出了) C2服務(wù)器上,對(duì)應(yīng)的用戶權(quán)限沒(méi)有清除(沒(méi)完全退出 )
原理分析Created with Rapha?l 2.1.2 Browser Browser C1 C1 cas cas C2 C2 logout request(1) C1 subject.logout(), redirect to cas (2) cas logout path(3) notify C2 the user had logout(4)
1,2,3都很正常,問(wèn)題出在第四步。
第四步僅僅是被SingleSignOutFilter攔截,根據(jù)service-ticket銷(xiāo)毀掉改用戶對(duì)應(yīng)的session,而并沒(méi)有調(diào)用shiro的subject.logout, 顯然,subject.logout是做了銷(xiāo)毀權(quán)限緩存等操作的
這樣就會(huì)導(dǎo)致最終C2上的用戶權(quán)限沒(méi)有被清除,若在此時(shí)用戶權(quán)限被修改,就會(huì)導(dǎo)致即使登出,C2上的權(quán)限也沒(méi)有刷新
解決方案 方案一權(quán)限緩存是可以設(shè)置過(guò)期時(shí)間的,那么簡(jiǎn)單點(diǎn),只要給權(quán)限緩存加上過(guò)期時(shí)間即可,這樣如果權(quán)限被修改,即使用戶不登出,在過(guò)期之后,權(quán)限也會(huì)被刷新
方案二http://howiefh./2015/05/19/shiro-cas-single-sign-on/ 有一個(gè)很詳細(xì)的說(shuō)明,但是沒(méi)仔細(xì)看,簡(jiǎn)單的說(shuō)就是使用ServletContainerSessionManager,即shiro自己的session管理,似乎可以解決問(wèn)題,但是未驗(yàn)證
方案三思路很簡(jiǎn)單,重寫(xiě)SingleSignOutFilter, 在登出的時(shí)候,調(diào)用subject.logout 即可。
奈何太年輕,這種方案有很多坑
坑一問(wèn)題:
Subject是由session中存放的一個(gè)key生成的,但是時(shí)序圖中第四步是有cas發(fā)起的請(qǐng)求,而不是用戶瀏覽器,即這個(gè)session中沒(méi)有Subject信息,shiro無(wú)法獲取到具體信息。
解決: SingleSignOutFilter中有存儲(chǔ)一份 service-ticket與session的映射關(guān)系,那么只要在第四步中 利用 service-ticket取到session,再?gòu)膕ession中取到SimplePrincipalCollection信息放入subject即可
坑二問(wèn)題:
subject不提供設(shè)置principal接口,service-ticket session映射關(guān)系未提供get接口
解決: 反射搞定,但是總覺(jué)得不靠譜呢。。
坑三問(wèn)題:
SingleSignOutFilter是在ShiroFilter chain之前,也就是說(shuō),如果重寫(xiě)SingleSignOutFilter,在里邊連一個(gè)不包含Principal的Subject都獲取不到,但是如果把這個(gè)SimplePrincipalCollection放到 shrioFilter之后,登錄的時(shí)候又會(huì)有問(wèn)題 這是一個(gè)雞生蛋和蛋生雞的問(wèn)題啊。。。
解決: 問(wèn)題總是能解決的,放在前邊后邊都不行,那么放一起吧。對(duì),把SingleSignOutFilter放到ShiroFilter之中, 原以為ShiroFilter會(huì)對(duì)符合過(guò)濾規(guī)則的做一個(gè)filter chain,結(jié)果并不是。
shiro會(huì)針對(duì)配置的filter規(guī)則,取第一個(gè)匹配的作為最終的filter,而后邊符合規(guī)則的就會(huì)被忽略掉
所以這里,要把SingleSignOutFilter和Shiro自己提供的CasFilter合并起來(lái),放在一起作為一個(gè)filter
方案三代碼經(jīng)過(guò)這么一折騰,于是就有了下面的代碼了
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); // 記錄完了之后,就調(diào)用CasFilter自己的doFilterInternal
return;
} else if (handler.isLogoutRequest(request)) { // 如果是登出
// 一堆的代碼,就是為了獲取SimplePrincipalCollection,設(shè)置到Subject里邊去,并在最后調(diào)用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之后,還要銷(xiāo)毀session SingleSignOutFilter做的事情
handler.destroySession(request);
return;
} else {
log.trace("Ignoring URI " + request.getRequestURI());
}
filterChain.doFilter(servletRequest, servletResponse);
}
代碼邏輯很簡(jiǎn)單,主要是要找到這么個(gè)解決方案,得一點(diǎn)點(diǎn)的調(diào)試和摸索,也是蠻有意思。 另外web.xml中的SingleSignOutFilter需要去掉,因?yàn)槲覀円呀?jīng)移到Shiro里邊了,但是Listener需要保留,并且需要自己重寫(xiě)(里邊有調(diào)用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 都走我們剛剛寫(xiě)的filter-->
/logout = logout
/** = user
</value>
</property>
</bean>
最后準(zhǔn)備找個(gè)時(shí)間寫(xiě)個(gè)cas的faq,畢竟在開(kāi)發(fā)過(guò)程中,遇到的很多常見(jiàn)問(wèn)題,很是煩躁。