前期准备
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/
单点登录已经成功 如下图
单点登出 执行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信息 如下
获取到具体的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时的信息
由于默认登出类型是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信息图如下
客户端登出消息请求发出成功之后,跳转回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客户端发出的登出消息请求是否成功,同时判断是否需要登出重定向到指定的地址,由于没有设置登出后需要重定向的地址,则service变量为null,直接返回FINISH_EVENT状态
跳转到finishLogout
logoutRedirectUrl为null,不重定向,跳转到logoutView
单点登出成功,返回登出成功页面
上面漏掉了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 server端发送的logoutMessage信息,解析logoutMessage信息,可以获取到登录时验证的ST票据信息,根据ST就可以删除客户端保存的session
删除session成功 返回false 不执行下一个filter cas client登出请求处理结束
if(HANDLER.process(request, response)) {
filterChain.doFilter(servletRequest, servletResponse);
}
至此,cas单点登出整个流程完整结束