1. 程式人生 > >CAS5.2x單點登入(九)---------cas服務端的退出原始碼解析以及客戶端儲存session及銷燬session原始碼的解析

CAS5.2x單點登入(九)---------cas服務端的退出原始碼解析以及客戶端儲存session及銷燬session原始碼的解析

上一節已經說了如何解決cas客戶端叢集的單點退出的方法,但是由於大家對原始碼還不夠了解,所以沒有寫程式碼上去,而我們這篇部落格就是基於上篇來進一步講解。
cas服務端的退出原始碼:
首先我們要找到這個jar包,因為我們是基於maven來管理這些包的,所以可以找到如下的這個包cas-server-core-logout,然後開啟這個目錄結構是如下所示的結構
這裡寫圖片描述
我們一看程式碼並不是很多,就這區區幾行,那我們就挑其中比較重要的來講,而我們要講的就是這個DefaultSingleLogoutServiceMessageHandler類,他是負責傳送退出請求到我們客戶端的,

public class
DefaultSingleLogoutServiceMessageHandler implements SingleLogoutServiceMessageHandler

他實現了SingleLogoutServiceMessageHandler 這個介面,找到這介面的時候,發現裡面就一個方法

   LogoutRequest handle(WebApplicationService singleLogoutService, String ticketId);

我們是不是看到很眼熟的東西啊,就是那個票據的東西,我們直接去實現類中看一下這個方法到底幹了什麼事情。

 @Override
public LogoutRequest handle(final WebApplicationService singleLogoutService, final String ticketId) { //判斷是否已經登出 if (singleLogoutService.isLoggedOutAlready()) { LOGGER.debug("Service [{}] is already logged out.", singleLogoutService); return null; } //處理服務登出請求
final WebApplicationService selectedService = WebApplicationService.class.cast( this.authenticationRequestServiceSelectionStrategies.resolveService(singleLogoutService)); LOGGER.debug("Processing logout request for service [{}]...", selectedService); //取出這個註冊的service服務的資訊 final RegisteredService registeredService = this.servicesManager.findServiceBy(selectedService); //判斷是否支援退出 if (!serviceSupportsSingleLogout(registeredService)) { LOGGER.debug("Service [{}] does not support single logout.", selectedService); return null; } LOGGER.debug("Service [{}] supports single logout and is found in the registry as [{}]. Proceeding...", selectedService, registeredService); //拿出logout的url,這個是我們自己註冊進去的,不知道可以參考之前的部落格 final URL logoutUrl = this.singleLogoutServiceLogoutUrlBuilder.determineLogoutUrl(registeredService, selectedService); LOGGER.debug("Prepared logout url [{}] for service [{}]", logoutUrl, selectedService); if (logoutUrl == null) { LOGGER.debug("Service [{}] does not support logout operations given no logout url could be determined.", selectedService); return null; } LOGGER.debug("Creating logout request for [{}] and ticket id [{}]", selectedService, ticketId); //封裝退出的訊息內容,將退出請求以及st封裝起來 final DefaultLogoutRequest logoutRequest = new DefaultLogoutRequest(ticketId, selectedService, logoutUrl); LOGGER.debug("Logout request [{}] created for [{}] and ticket id [{}]", logoutRequest, selectedService, ticketId); //判斷是哪種模式下的退出請求,cas伺服器分為三種,應該我之前講過,可以自己看看 final RegisteredService.LogoutType type = registeredService.getLogoutType() == null ? RegisteredService.LogoutType.BACK_CHANNEL : registeredService.getLogoutType(); LOGGER.debug("Logout type registered for [{}] is [{}]", selectedService, type); switch (type) { case BACK_CHANNEL: if (performBackChannelLogout(logoutRequest)) { logoutRequest.setStatus(LogoutRequestStatus.SUCCESS); } else { logoutRequest.setStatus(LogoutRequestStatus.FAILURE); LOGGER.warn("Logout message is not sent to [{}]; Continuing processing...", singleLogoutService.getId()); } break; default: LOGGER.debug("Logout operation is not yet attempted for [{}] given logout type is set to [{}]", selectedService, type); logoutRequest.setStatus(LogoutRequestStatus.NOT_ATTEMPTED); break; } return logoutRequest; }

是不是看這這麼多程式碼就不願意看啊,可是上面的註釋我們也已經寫的很詳細了,然後一步一步達到我們的退出請求的時候發現有一個這個方法performBackChannelLogout

try {
            LOGGER.debug("Creating back-channel logout request based on [{}]", request);
            final String logoutRequest = this.logoutMessageBuilder.create(request);
            final WebApplicationService logoutService = request.getService();
            //將傳送退出後的設定為已傳送
            logoutService.setLoggedOutAlready(true);

            LOGGER.debug("Preparing logout request for [{}] to [{}]", logoutService.getId(), request.getLogoutUrl());
            封裝訊息
            final LogoutHttpMessage msg = new LogoutHttpMessage(request.getLogoutUrl(), logoutRequest, this.asynchronous);
            LOGGER.debug("Prepared logout message to send is [{}]. Sending...", msg);
            傳送訊息
            return this.httpClient.sendMessageToEndPoint(msg);
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
        }

這個方法就簡單多了,他就是我們要找的傳送退出請求到客戶端的地方,其實也不是很難
最後還有一個是sendMessageToEndPoint這個方法,因為我們還不知道他傳送了什麼以及使用什麼方式傳送的呢?

 @Override
    public boolean sendMessageToEndPoint(final HttpMessage message) {
        try {
        //以post的形式傳送
            final HttpPost request = new HttpPost(message.getUrl().toURI());
            request.addHeader("Content-Type", message.getContentType());

            final StringEntity entity = new StringEntity(message.getMessage(), ContentType.create(message.getContentType()));
            request.setEntity(entity);

            final ResponseHandler<Boolean> handler = response -> response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
            LOGGER.debug("Created HTTP post message payload [{}]", request);
            final HttpRequestFutureTask<Boolean> task = this.requestExecutorService.execute(request, HttpClientContext.create(), handler);
            if (message.isAsynchronous()) {
                return true;
            }
            return task.get();
        } catch (final RejectedExecutionException e) {
            LOGGER.warn("Execution rejected", e);
            return false;
        } catch (final Exception e) {
            LOGGER.debug("Unable to send message", e);
            return false;
        }
    }

這個一看就清楚了吧,其實就是一post的方式傳送退出請求到客戶端,然後夾帶著一些資訊。服務端退出的原始碼大致就是這麼簡單,還有其他幾個大家就自己看吧,其實看原始碼最簡單的方式就是debug,一步一步走就能明白他幹了什麼事情。

客戶端儲存session和銷燬session的原始碼
首先我們還是要先下載cas-client-core的原始碼,然後開啟是這樣子的
這裡寫圖片描述
看這客戶端的原始碼是不是比較多啊,其實也不是很難,也就那幾個重要的類(這個之後再講吧)。我們今天主要還是講session的儲存和銷燬。大家可以直接找到SingleSignOutHandler這個類,看到如下的方法

  public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
        if (isTokenRequest(request)) {
            logger.trace("Received a token request");
            recordSession(request);
            return true;
        } 

        if (isLogoutRequest(request)) {
            logger.trace("Received a logout request");
            destroySession(request);
            return false;
        } 
        logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
        return true;
    }

是不是看到一個recordSession和destroySession的方法。其實這兩個就是我們今天的主角,
首先我們看recordSession吧;

 private void recordSession(final HttpServletRequest request) {
        final HttpSession session = request.getSession(this.eagerlyCreateSessions);

        if (session == null) {
            logger.debug("No session currently exists (and none created).  Cannot record session information for single sign out.");
            return;
        }

        final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);
        logger.debug("Recording session for token {}", token);

        try {
            this.sessionMappingStorage.removeBySessionById(session.getId());
        } catch (final Exception e) {
            // ignore if the session is already marked as invalid. Nothing we can do!
        }
        sessionMappingStorage.addSessionById(token, session);

我們可以清楚的看到我們客戶端生成session的時候會將token和session儲存到 sessionMappingStorage.addSessionById(token, session);這裡面,而這其中的token其實就是St,
這裡寫圖片描述
在生成session的同時,會將session和st票據存到map中,sessionid和st也存到map中,這個時候我們就可以在這裡增加redis儲存st和sessionid的操作。
至於怎麼儲存就不用我多說了吧,直接引入jedis的包,然後操作就行了。
而destroySession方法其實也簡單

 String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
        if (CommonUtils.isBlank(logoutMessage)) {
            logger.error("Could not locate logout message of the request from {}", this.logoutParameterName);
            return;
        }

        if (!logoutMessage.contains("SessionIndex")) {
            logoutMessage = uncompressLogoutMessage(logoutMessage);
        }

        logger.trace("Logout request:\n{}", logoutMessage);
        final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
        if (CommonUtils.isNotBlank(token)) {
            final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);

            if (session != null) {
                final String sessionID = session.getId();
                logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);

                try {
                    System.err.println("清除session++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
                    session.invalidate();
//                    j.del(token);
                } catch (final IllegalStateException e) {
                    logger.debug("Error invalidating session.", e);
                }finally{
//                  RedisPool.returnResource(j);
                }
                this.logoutStrategy.logout(request);
            }else{
//               //如果在上面退出的時候沒有發現session,那麼一定是走其他的實列,這時候就自己手動去redis更新st票據sessionid的快取
//              if("".equals(RedisPool.appName)){
//                  j.hset("spring:session:sessions:"+j.get(token), "maxInactiveInterval", "0");
//              }else{
//                  j.hset("spring:session::sessions:"+j.get(token), "maxInactiveInterval", "0");
//              }
//              j.del(token);
//              RedisPool.returnResource(j);
            }
        }

他會根據傳過來的st去map查詢sesion,如果查到就銷燬,而我這邊自己定製了下,如果沒有查到就去redis查詢,並將查出來的sessionid通過某種方式去清除redis改sessionid的session。這樣就達到我們叢集的單點退出功能,而之前說的廣播方式其實也是在這加的。可能還有更好的方式,歡迎大家指點。

其實原始碼真心不是很難,只是我們覺得難的原因是因為太多了,毫無頭緒,這個時候就推薦你使用debug,這樣既能瞭解每一步的執行方式,還能清楚程式碼的邏輯結構。