在shiro-cas中实现 Jasig-cas的Single Sign Out 功能
1 Single Sign Out 功能
即单点登出功能。也就是在任意子系统进行登出操作后,其他子系统会自动登出。
实际CAS登出的步骤为
所以每个子系统都需要实现一个sso登出响应。
cas-client-core包中有Single Sign Out的Session容器实现。
具体在包 org.jasig.cas.client.session 中。
2 实现Shiro的SSO登出功能
1 实现CasSecurityManager
主要目的是为了在登陆成功后保存 ST票据,并与 Shiro的sessionId进行关系映射。
/** * 安全管理中心。<br> * 主要目的是保存session和ticket之间的关系。 * @author Weicl * @since 2016.4.25 */ public class CasSecurityManager extends DefaultWebSecurityManager{ Logger logger = LoggerFactory.getLogger(getClass()); @Override protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) { if (token instanceof CasToken) { logger.info("save token info: " + token.getCredentials() + " -> " + subject.getSession(false).getId()); SsoUtils.putTokenCache((String)token.getCredentials(), subject.getSession(false).getId()); subject.getSession(false).setAttribute("_serviceTicket_", token.getCredentials()); } super.onSuccessfulLogin(token, info, subject); } }
PS: SsoUtils的putTokenCache。可以用ehcache或HashMap实现,其实没关系。
2 实现LogoutSloFilter
这个拦截器用来处理sso登出请求。
/** * 单点登出处理 * @author Weicl * @since 2016.4.25 */ public class LogoutSloFilter extends AdviceFilter{ private final Logger logger = LoggerFactory.getLogger(getClass()); private final Pattern pattern = Pattern.compile("<samlp:SessionIndex>([^<]*)</samlp:SessionIndex>"); @Autowired private NativeSessionManager nativeSessionManager; @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { try { String logoutRequest = request.getParameter("logoutRequest"); String serviceTicket = extractServiceTicket(logoutRequest); logger.info(" slo serviceTicket : " + serviceTicket); String sessionId = (String)SsoUtils.getTokenCache(serviceTicket); nativeSessionManager.stop(new DefaultSessionKey(sessionId)); } catch (Exception e) { e.printStackTrace(); } response.getWriter().write("OK"); return false; } /** * 获取登出请求中的Ticket * @param logoutRequest * @return */ private String extractServiceTicket(String logoutRequest) { Matcher matcher = pattern.matcher(logoutRequest); if (matcher.find()) { return matcher.group(1); } return ""; } }
3 实现TicketSessionListener
这个类的作用是当session终止的时候,释放 SsoUtil 中的tokenCache。因为应用的session不存在了,保存这个映射关系也没有意义,而且浪费缓存空间。
/** * 票据及session监听器 * @author Weicl * @since 2016.4.25 */ public class TicketSessionListener implements SessionListener{ Logger logger = LoggerFactory.getLogger(getClass()); @Override public void onStart(Session session) { } @Override public void onStop(Session session) { logger.info("==============================="); logger.info("stop session:" + session.getId()); String ticket = (String)session.getAttribute("_serviceTicket_"); if (ticket != null) { logger.info("remove serviceTicket: " + ticket); SsoUtils.removeTokenCache(ticket); } } @Override public void onExpiration(Session session) { onStop(session); } }
4 配置到spring中
一下为添加/修正的关键代码。其他shiro的配置这边就不贴出来了。
<!-- Shiro权限过滤过滤器定义 --> <bean name="shiroFilterChainDefinitions" class="java.lang.String"> <constructor-arg> <value> /static/** = anon /cas = cas /login = authc /logout = logout /logoutSlo = logoutSlo /** = user </value> </constructor-arg> </bean> <!-- 安全认证过滤器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="${cas.server.url}/login?service=${cas.project.url}${adminPath}/cas" /> <!-- <property name="loginUrl" value="${adminPath}/login" /> --> <property name="successUrl" value="${adminPath}/login" /> <property name="filters"> <map> <entry key="cas" value-ref="casFilter"/> <entry key="authc" value-ref="formAuthenticationFilter"/> <entry key="logout" value-ref="logoutFilter"></entry> <entry key="logoutSlo" value-ref="logoutSloFilter"></entry> </map> </property> <property name="filterChainDefinitions"> <ref bean="shiroFilterChainDefinitions"/> </property> </bean> <bean id="logoutSloFilter" class="cn.xxxxxx.base.modules.sys.security.LogoutSloFilter"> </bean> <!-- 定义Shiro安全管理配置 --> <bean id="securityManager" class="cn.xxxxxx.base.common.security.shiro.session.CasSecurityManager"> <!-- <property name="realm" ref="systemAuthorizingRealm" /> --> <property name="realm" ref="systemCasRealm" /> <property name="sessionManager" ref="sessionManager" /> <property name="cacheManager" ref="shiroCacheManager" /> </bean>