上一节已经说了如何解决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 handler = response -> response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
LOGGER.debug("Created HTTP post message payload [{}]", request);
final HttpRequestFutureTask 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,这样既能了解每一步的执行方式,还能清楚代码的逻辑结构。