cas单点登出流程源码分析

前期准备

cas版本4.1.11

cas client

http://www.eric.cas.client.com:8081/cas/index.do

cas server

http://www.eric.cas.server.com:8080/

单点登录已经成功 如下图

cas单点登出流程源码分析_第1张图片
image

单点登出 执行logout-webflow.xml流程





  
    
    
  

  
    
    
    
  

  
    
    
    
  

  
    
  

  
    
  

  

  



执行TerminateSessionAction的terminate方法

  
    
    
  
/**
     * Terminates the CAS SSO session by destroying the TGT (if any) and removing cookies related to the SSO session.
     *
     * @param context Request context.
     *
     * @return "success"
     */
    public Event terminate(final RequestContext context) {
        // in login's webflow : we can get the value from context as it has already been stored
        String tgtId = WebUtils.getTicketGrantingTicketId(context);
        // for logout, we need to get the cookie's value
        if (tgtId == null) {
            final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
            // 从cookie中获取tgtId
            tgtId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
        }
        if (tgtId != null) {
            // 构造LogoutRequest 并放到FlowScope作用域中
            WebUtils.putLogoutRequests(context, this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId));
        }
        final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
        this.ticketGrantingTicketCookieGenerator.removeCookie(response);
        this.warnCookieGenerator.removeCookie(response);
        return this.eventFactorySupport.success(this);
    }

该方法从cookie中获取tgtId,然后根据tgtId处理业务逻辑。

其中WebUtils的putLogoutRequests源码如下

    /**
     * Put logout requests into flow scope.
     *
     * @param context the context
     * @param requests the requests
     */
    public static void putLogoutRequests(final RequestContext context, final List requests) {
        context.getFlowScope().put("logoutRequests", requests);
    }
    

putLogoutRequests只是把构造好的LogoutRequest放到FlowScope作用域

其中使用以下方法销毁服务端TGT

this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId)

该方法实际调用实现类CentralAuthenticationServiceImpl的destroyTicketGrantingTicket方法

    /**
     * {@inheritDoc}
     * Destroy a TicketGrantingTicket and perform back channel logout. This has the effect of invalidating any
     * Ticket that was derived from the TicketGrantingTicket being destroyed. May throw an
     * {@link IllegalArgumentException} if the TicketGrantingTicket ID is null.
     *
     * @param ticketGrantingTicketId the id of the ticket we want to destroy
     * @return the logout requests.
     */
    @Audit(
            action = "TICKET_GRANTING_TICKET_DESTROYED",
            actionResolverName = "DESTROY_TICKET_GRANTING_TICKET_RESOLVER",
            resourceResolverName = "DESTROY_TICKET_GRANTING_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "DESTROY_TICKET_GRANTING_TICKET_TIMER")
    @Metered(name = "DESTROY_TICKET_GRANTING_TICKET_METER")
    @Counted(name = "DESTROY_TICKET_GRANTING_TICKET_COUNTER", monotonic = true)
    @Override
    public List destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) {
        try {
            logger.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId);
            // 根据tgtId查询内存或者缓存中是否存在有效的TGT票据
            final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
            logger.debug("Ticket found. Processing logout requests and then deleting the ticket...");
            // 构造客户端登出请求
            final List logoutRequests = logoutManager.performLogout(ticket);
            this.ticketRegistry.deleteTicket(ticketGrantingTicketId);

            return logoutRequests;
        } catch (final InvalidTicketException e) {
            logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
        }
        return Collections.emptyList();
    }

从源码来看destroyTicketGrantingTicket方法主要完成了两件事情

  • 删除cas server存储的TGT
  • 通知cas client删除登录session

继续debug,看看cas server具体是怎么通知cas client处理登出消息的,构造客户端登出请求的方法

logoutManager.performLogout(ticket);

该方法实际执行实现类LogoutManagerImpl的performLogout方法 源码如下

@Override
    public List performLogout(final TicketGrantingTicket ticket) {
        // 获取登录的service信息 一个TGT包含多个service
        final Map services = ticket.getServices();
        final List logoutRequests = new ArrayList<>();
        // if SLO is not disabled
        if (!this.singleLogoutCallbacksDisabled) {
            // through all services
            for (final Map.Entry entry : services.entrySet()) {
                // it's a SingleLogoutService, else ignore
                final Service service = entry.getValue();
                if (service instanceof SingleLogoutService) {
                    // 根据service构建具体的客户端登出请求
                    final LogoutRequest logoutRequest = handleLogoutForSloService((SingleLogoutService) service, entry.getKey());
                    if (logoutRequest != null) {
                        LOGGER.debug("Captured logout request [{}]", logoutRequest);
                        logoutRequests.add(logoutRequest);
                    }
                }
            }
        }

        return logoutRequests;
    }

该方法首先获取单点登录的cas client信息 如下

cas单点登出流程源码分析_第2张图片
image

获取到具体的service之后,我们需要根据service来构建具体的客户端登出请求,具体代码如下

/**
     * Handle logout for slo service.
     *
     * @param singleLogoutService the service
     * @param ticketId the ticket id
     * @return the logout request
     */
    private LogoutRequest handleLogoutForSloService(final SingleLogoutService singleLogoutService, final String ticketId) {
        if (!singleLogoutService.isLoggedOutAlready()) {

            final RegisteredService registeredService = servicesManager.findServiceBy(singleLogoutService);
            if (serviceSupportsSingleLogout(registeredService)) {
                // 生成logoutUrl
                final URL logoutUrl = determineLogoutUrl(registeredService, singleLogoutService);
                final DefaultLogoutRequest logoutRequest = new DefaultLogoutRequest(ticketId, singleLogoutService, logoutUrl);
                // 判断登出类型 默认为BACK_CHANNEL
                final LogoutType type = registeredService.getLogoutType() == null
                        ? LogoutType.BACK_CHANNEL : registeredService.getLogoutType();

                switch (type) {
                    case BACK_CHANNEL:
                        if (performBackChannelLogout(logoutRequest)) {
                            logoutRequest.setStatus(LogoutRequestStatus.SUCCESS);
                        } else {
                            logoutRequest.setStatus(LogoutRequestStatus.FAILURE);
                            LOGGER.warn("Logout message not sent to [{}]; Continuing processing...", singleLogoutService.getId());
                        }
                        break;
                    default:
                        logoutRequest.setStatus(LogoutRequestStatus.NOT_ATTEMPTED);
                        break;
                }
                return logoutRequest;
            }

        }
        return null;
    }

以下是debug时的信息

cas单点登出流程源码分析_第3张图片
image

由于默认登出类型是BACK_CHANNEL,则跳转到代码performBackChannelLogout(logoutRequest)

/**
     * Log out of a service through back channel.
     *
     * @param request the logout request.
     * @return if the logout has been performed.
     */
    private boolean performBackChannelLogout(final LogoutRequest request) {
        try {
            final String logoutRequest = this.logoutMessageBuilder.create(request);
            final SingleLogoutService logoutService = request.getService();
            logoutService.setLoggedOutAlready(true);
            
            LOGGER.debug("Sending logout request for: [{}]", logoutService.getId());
            // 生成客户端登出http消息
            final LogoutHttpMessage msg = new LogoutHttpMessage(request.getLogoutUrl(), logoutRequest);
            LOGGER.debug("Prepared logout message to send is [{}]", msg);
            // 发送登出消息到客户端
            return this.httpClient.sendMessageToEndPoint(msg);
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
        return false;
    }

具体debug信息图如下

cas单点登出流程源码分析_第4张图片
image

客户端登出消息请求发出成功之后,跳转回CentralAuthenticationServiceImpl类的destroyTicketGrantingTicket方法

/**
     * {@inheritDoc}
     * Destroy a TicketGrantingTicket and perform back channel logout. This has the effect of invalidating any
     * Ticket that was derived from the TicketGrantingTicket being destroyed. May throw an
     * {@link IllegalArgumentException} if the TicketGrantingTicket ID is null.
     *
     * @param ticketGrantingTicketId the id of the ticket we want to destroy
     * @return the logout requests.
     */
    @Audit(
            action = "TICKET_GRANTING_TICKET_DESTROYED",
            actionResolverName = "DESTROY_TICKET_GRANTING_TICKET_RESOLVER",
            resourceResolverName = "DESTROY_TICKET_GRANTING_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "DESTROY_TICKET_GRANTING_TICKET_TIMER")
    @Metered(name = "DESTROY_TICKET_GRANTING_TICKET_METER")
    @Counted(name = "DESTROY_TICKET_GRANTING_TICKET_COUNTER", monotonic = true)
    @Override
    public List destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) {
        try {
            logger.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId);
            final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
            logger.debug("Ticket found. Processing logout requests and then deleting the ticket...");
            final List logoutRequests = logoutManager.performLogout(ticket);
            this.ticketRegistry.deleteTicket(ticketGrantingTicketId);
            return logoutRequests;
        } catch (final InvalidTicketException e) {
            logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
        }
        return Collections.emptyList();
    }

执行删除TGT操作

this.ticketRegistry.deleteTicket(ticketGrantingTicketId);

将发出的登出请求信息返回给TerminateSessionAction的terminate的方法,然后执行删除TGC等cookie操作,具体代码如下

        // 删除TGC cookie 和 warnCookie
        final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
        this.ticketGrantingTicketCookieGenerator.removeCookie(response);
        this.warnCookieGenerator.removeCookie(response);
        return this.eventFactorySupport.success(this);

设置事件状态为成功 直接跳转到logout-webflow的下一个流程

  
    
    
    
  

执行logoutAction的doInternalExecute方法

@Override
    protected Event doInternalExecute(final HttpServletRequest request, final HttpServletResponse response,
            final RequestContext context) throws Exception {

        boolean needFrontSlo = false;
        putLogoutIndex(context, 0);
        final List logoutRequests = WebUtils.getLogoutRequests(context);
        if (logoutRequests != null) {
            for (final LogoutRequest logoutRequest : logoutRequests) {
                // if some logout request must still be attempted
                if (logoutRequest.getStatus() == LogoutRequestStatus.NOT_ATTEMPTED) {
                    needFrontSlo = true;
                    break;
                }
            }
        }

        final String service = request.getParameter("service");
        if (this.followServiceRedirects && service != null) {
            final Service webAppService = new SimpleWebApplicationServiceImpl(service);
            final RegisteredService rService = this.servicesManager.findServiceBy(webAppService);

            if (rService != null && rService.getAccessStrategy().isServiceAccessAllowed()) {
                context.getFlowScope().put("logoutRedirectUrl", service);
            }
        }

        // there are some front services to logout, perform front SLO
        if (needFrontSlo) {
            return new Event(this, FRONT_EVENT);
        } else {
            // otherwise, finish the logout process
            return new Event(this, FINISH_EVENT);
        }
    }

此时debug信息如下

cas单点登出流程源码分析_第5张图片
image

该方法主要是判断前面给cas客户端发出的登出消息请求是否成功,同时判断是否需要登出重定向到指定的地址,由于没有设置登出后需要重定向的地址,则service变量为null,直接返回FINISH_EVENT状态

  
    
    
    
  
  
  
    
    
    
  

跳转到finishLogout

  
    
  

logoutRedirectUrl为null,不重定向,跳转到logoutView

  

单点登出成功,返回登出成功页面

cas单点登出流程源码分析_第6张图片
image

上面漏掉了cas server发送LogoutRequest请求到cas client,cas client处理登出请求的过程

cas client处理登出请求的过程

请求到达cas client,首先被过滤器SingleSignOutFilter拦截,执行doFilter方法

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        if(!this.handlerInitialized.getAndSet(true)) {
            HANDLER.init();
        }
        // 登出请求处理
        if(HANDLER.process(request, response)) {
            filterChain.doFilter(servletRequest, servletResponse);
        }

    }

执行SingleSignOutHandler的process方法

public boolean process(HttpServletRequest request, HttpServletResponse response) {
        if(this.isTokenRequest(request)) {
            this.logger.trace("Received a token request");
            this.recordSession(request);
            return true;
        } else if(this.isBackChannelLogoutRequest(request)) {
            this.logger.trace("Received a back channel logout request");
            this.destroySession(request);
            return false;
        } else if(this.isFrontChannelLogoutRequest(request)) {
            this.logger.trace("Received a front channel logout request");
            this.destroySession(request);
            String redirectionUrl = this.computeRedirectionToServer(request);
            if(redirectionUrl != null) {
                CommonUtils.sendRedirect(response, redirectionUrl);
            }

            return false;
        } else {
            this.logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
            return true;
        }
    }

由于是采用BackChannel的方式登出的请求,则直接销毁客户端保存的session

private void destroySession(HttpServletRequest request) {
        String logoutMessage;
        if(this.isFrontChannelLogoutRequest(request)) {
            logoutMessage = this.uncompressLogoutMessage(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName));
        } else {
            logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
        }

        this.logger.trace("Logout request:\n{}", logoutMessage);
        // 登录时验证的ST票据信息
        String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
        if(CommonUtils.isNotBlank(token)) {
            // 根据ST查询session 并删除本地缓存的session  存储类HashMapBackedSessionMappingStorage
            HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
            if(session != null) {
                String sessionID = session.getId();
                this.logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);

                try {
                    //清除当前session的所有相关信息
                    session.invalidate();
                } catch (IllegalStateException var7) {
                    this.logger.debug("Error invalidating session.", var7);
                }

                this.logoutStrategy.logout(request);
            }
        }

    }

此时debug信息

cas单点登出流程源码分析_第7张图片
image

可以看到cas server端发送的logoutMessage信息,解析logoutMessage信息,可以获取到登录时验证的ST票据信息,根据ST就可以删除客户端保存的session

cas单点登出流程源码分析_第8张图片
image

删除session成功 返回false 不执行下一个filter cas client登出请求处理结束

        if(HANDLER.process(request, response)) {
            filterChain.doFilter(servletRequest, servletResponse);
        }

至此,cas单点登出整个流程完整结束

你可能感兴趣的:(cas单点登出流程源码分析)