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,這樣既能瞭解每一步的執行方式,還能清楚程式碼的邏輯結構。