最近遇到一个非常麻烦的问题,cas+shiro 统一注销的失败,cas正常注销掉,但是应用里面的用户信息没有被注销掉
跟踪问题:
首先怀疑SingleSignOutHttpSessionListener监听器没有正常工作没有把应用的session注销掉,这里可以跟大家讲一下cas 客户端及服务器的注销原理
1,客户端发送一个注销请求到cas server,跟踪casorg.jasig.cas.CentralAuthenticationServiceImpl类的destroyTicketGrantingTicket注销方法,
服务端注销代码
@Audit( action="TICKET_GRANTING_TICKET_DESTROYED", actionResolverName="DESTROY_TICKET_GRANTING_TICKET_RESOLVER", resourceResolverName="DESTROY_TICKET_GRANTING_TICKET_RESOURCE_RESOLVER") @Profiled(tag = "DESTROY_TICKET_GRANTING_TICKET", logFailuresSeparately = false) @Transactional(readOnly = false) @Override public List<LogoutRequest> destroyTicketGrantingTicket(final String ticketGrantingTicketId) { Assert.notNull(ticketGrantingTicketId); logger.debug("Removing ticket [{}] from registry.", ticketGrantingTicketId); final TicketGrantingTicket ticket = this.ticketRegistry.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class); if (ticket == null) { logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId); return Collections.emptyList(); } logger.debug("Ticket found. Processing logout requests and then deleting the ticket..."); //在这里cas server会去根据客户端带过来的ticket找到所有在cas server服务注册过的cas client server,让后对这些客户端服务发送一个http请求,客户端接受请求,删除//本身的ticket及注销session,cas server发送请求看下一段代码 final List<LogoutRequest> logoutRequests = logoutManager.performLogout(ticket); this.ticketRegistry.deleteTicket(ticketGrantingTicketId); return logoutRequests; } @Override
public List<LogoutRequest> performLogout(final TicketGrantingTicket ticket) { final Map<String, Service> services; // synchronize the retrieval of the services and their cleaning for the TGT // to avoid concurrent logout mess ups synchronized (ticket) { services = ticket.getServices(); ticket.removeAllServices(); } ticket.markTicketExpired(); final List<LogoutRequest> logoutRequests = new ArrayList<LogoutRequest>(); // if SLO is not disabled if (!disableSingleSignOut) { // through all services //循环遍历客户端的server for (final String ticketId : services.keySet()) { final Service service = services.get(ticketId); // it‘s a SingleLogoutService, else ignore if (service instanceof SingleLogoutService) { final SingleLogoutService singleLogoutService = (SingleLogoutService) service; // the logout has not performed already if (!singleLogoutService.isLoggedOutAlready()) { final LogoutRequest logoutRequest = new LogoutRequest(ticketId, singleLogoutService); // always add the logout request logoutRequests.add(logoutRequest); final RegisteredService registeredService = servicesManager.findServiceBy(service); // the service is no more defined, or the logout type is not defined or is back channel if (registeredService == null || registeredService.getLogoutType() == null || registeredService.getLogoutType() == LogoutType.BACK_CHANNEL) { // perform back channel logout //向客户端发送一个请求, if (performBackChannelLogout(logoutRequest)) { logoutRequest.setStatus(LogoutRequestStatus.SUCCESS); } else { logoutRequest.setStatus(LogoutRequestStatus.FAILURE); LOGGER.warn("Logout message not sent to [{}]; Continuing processing...", singleLogoutService.getId()); } } } } } } return logoutRequests; }
下面在来看一下客户端接收的代码,客户段单点注销必须配置SingleSignOutFilter,前文cas server发送一个注销请求回来的时候会被接收处理
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; //重点的两个IF判断,同时也是逻辑处理,第一是判断是不是第一次登录的时候会进入方法往cas SessionMappingStorage的添加ticket,sessionId键值对 if (handler.isTokenRequest(request)) { LOG.warn("第{}次进来映射,sessionId={}", index++, request.getSession().getId()); handler.recordSession(request); //判断是否是注销请求,如果是注销请求进入逻辑,注销session同时删除SessionMappingStorage的键值对(就是这里实现了统一注销) } else if (handler.isLogoutRequest(request)) { LOG.warn("第{}进来删除映射,sessionId={}", removeIndex++, request.getSession().getId()); handler.destroySession(request); // Do not continue up filter chain return; } else { log.trace("Ignoring URI " + request.getRequestURI()); } filterChain.doFilter(servletRequest, servletResponse); }
回到原来的主题,通过日志打印检查SingleSignOutHttpSessionListener有正常监听到session的注销行动,也完成了我想要的操作,此路不通了..
第二个猜想SingleSignOutHttpSessionListener监听到完成我想要的操作了,以及前文的注销handler.destroySession(request);所注销的session与我保存到键值对里面的不是同一个session
通过打印日志:发现一个问题
[WARN ] 2016-01-27 21:18:17,423 --> com.chenrd.shiro.SingleSignOutFilter.doFilter(SingleSignOutFilter.java:63): 第6次进来映射,sessionId=7EC4C5CFCEBA41C47254F4DCC2497EAA
[DEBUG] 2016-01-27 21:18:17,424 --> org.jasig.cas.client.session.SingleSignOutHandler.recordSession(SingleSignOutHandler.java:118): Recording session for token ST-248-U4CpR4jgP0qkrVQP4Q9N-cas01.example.org
[DEBUG] 2016-01-27 21:18:39,051 --> org.jasig.cas.client.util.CommonUtils.safeGetParameter(CommonUtils.java:291): safeGetParameter called on a POST HttpServletRequest for LogoutRequest. Cannot complete check safely. Reverting to standard behavior for this Parameter
[WARN ] 2016-01-27 21:18:59,655 --> com.chenrd.shiro.ShiroCheckFilter.doFilter(ShiroCheckFilter.java:67): BB1616D432877F2D4998F5FD88BEA7B6
上面只是两个重要日志,一个是登录成功进行映射的日志,下面的是注销时的日志
上面说的发现的问题就是,我的一次登录操作里面尽然多次输出了上面两个操作,说面session被多次的注销及创建(两个日志不对等),那么第二个猜想就成立,没有注销掉正确的session
那么问题很明显了,下面就是说明一下为什么会发送这样的情况,通过跟踪代码,找到答案:原来我的项目中,使用了web service,由于我的业务需要,在登录的时候会有web service 请求来访问项目,web service请求同样也是http请求过来,其中没有带上cas登录的身份信息,SingleSignOutFilter会进入注销逻辑去注销session。
/jaxws/services/**=anon我的shiro配置了web service不进行身份验证也是无效的,因为SingleSignOutFilter过滤器的优先级在shiro过滤前面,所以才会发送这个问题。