CAS-server登出如何保证客户端登出的?
大致流程描述:server端登出会根据在webflow scope中存储的TGT信息查询ticket即st,然后根据st携带应用信息向客户端发送登出POST请求。客户端需要配置SingleSignOutFilter拦截器,在拦截器中将客户端的session失效。
源码分析server端cas源码5.3.3
TerminateSessionAction类
public Event doExecute(final RequestContext requestContext) {
boolean terminateSession = true;
if (logoutProperties.isConfirmLogout()) {
terminateSession = isLogoutRequestConfirmed(requestContext);
}
if (terminateSession) {
//确认登出
return terminate(requestContext);
}
return this.eventFactorySupport.event(this, CasWebflowConstants.STATE_ID_WARN);
}
public Event terminate(final RequestContext context) {
final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context);
//从上下文中获取tgt
String tgtId = WebUtils.getTicketGrantingTicketId(context);
//如果上下文中拿不到tgt尝试从cookie中获取tgt
if (StringUtils.isBlank(tgtId)) {
tgtId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
}
if (StringUtils.isNotBlank(tgtId)) {
LOGGER.debug("Destroying SSO session linked to ticket-granting ticket [{}]", tgtId);
//构造登出请求到发送到客户端
final List<LogoutRequest> logoutRequests = this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId);
WebUtils.putLogoutRequests(context, logoutRequests);
}
LOGGER.debug("Removing CAS cookies");
this.ticketGrantingTicketCookieGenerator.removeCookie(response);
this.warnCookieGenerator.removeCookie(response);
destroyApplicationSession(request, response);
LOGGER.debug("Terminated all CAS sessions successfully.");
if (StringUtils.isNotBlank(logoutProperties.getRedirectUrl())) {
WebUtils.putLogoutRedirectUrl(context, logoutProperties.getRedirectUrl());
return this.eventFactorySupport.event(this, CasWebflowConstants.STATE_ID_REDIRECT);
}
return this.eventFactorySupport.success(this);
}
DefaultCentralAuthenticationService 类
public List<LogoutRequest> destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
try {
//根据tgtId获取tgt对象,对象中存储了客户端信息
final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
AuthenticationCredentialsThreadLocalBinder.bindCurrent(ticket.getAuthentication());
//根据ticket对象构造客户端请求信息
final List<LogoutRequest> logoutRequests = this.logoutManager.performLogout(ticket);
//删除tgt,让server端登出
deleteTicket(ticketGrantingTicketId);
//发送tgt销毁时间
doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket));
return logoutRequests;
} catch (final InvalidTicketException e) {
LOGGER.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
}
return new ArrayList<>(0);
}
DefaultLogoutManager 类
public List<LogoutRequest> performLogout(final TicketGrantingTicket ticket) {
LOGGER.info("Performing logout operations for [{}]", ticket.getId());
//客户端登出是否开启未开启不执行
if (this.singleLogoutCallbacksDisabled) {
return new ArrayList<>(0);
}
//根据ticker对象构造客户端登出
final List<LogoutRequest> logoutRequests = performLogoutForTicket(ticket);
//客户端登出之后要invoke响应的handle做一些操作,比如删除ticket信息
this.logoutExecutionPlan.getLogoutHandlers().forEach(h -> {
h.handle(ticket);
});
return logoutRequests;
}
DefaultLogoutManager类
private List<LogoutRequest> performLogoutForTicket(final TicketGrantingTicket ticketToBeLoggedOut) {
//获取这个tgt下发的st和授权的service
val streamServices = Stream.concat(Stream.of(ticketToBeLoggedOut.getServices()),
Stream.of(ticketToBeLoggedOut.getProxyGrantingTickets()));
//将map转换成list并且筛选掉不是WebApplicationService的应用
val logoutServices = streamServices.map(Map::entrySet).flatMap(Set::stream)
.filter(entry -> entry.getValue() instanceof WebApplicationService).filter(Objects::nonNull)
.map(entry -> Pair.of(entry.getKey(), (WebApplicationService) entry.getValue()))
.collect(Collectors.toList());
//删除需要登出的oauth token
destroyOAuth20Tokens(ticketToBeLoggedOut);
val sloHandlers = logoutExecutionPlan.getSingleLogoutServiceMessageHandlers();
return logoutServices.stream().map(
entry -> sloHandlers.stream().filter(handler -> handler.supports(entry.getValue())).map(handler -> {
val service = entry.getValue();
log.debug("Handling single logout callback for [{}]", service.getId());
return handler.handle(service, entry.getKey(), ticketToBeLoggedOut);
}).flatMap(Collection::stream).filter(Objects::nonNull).collect(Collectors.toList()))
.flatMap(Collection::stream).distinct().collect(Collectors.toList());
}
BaseSingleLogoutServiceMessageHandler 类
public Collection<LogoutRequest> handle(final WebApplicationService singleLogoutService, final String ticketId,
final TicketGrantingTicket ticketGrantingTicket) {
if (singleLogoutService.isLoggedOutAlready()) {
return new ArrayList<>(0);
}
//根据应用类型获取service主要兼容ouath对接的应用
val selectedService = (WebApplicationService) this.authenticationRequestServiceSelectionStrategies
.resolveService(singleLogoutService);
//根据service查询判断注册应用的有效期等等
val registeredService = this.servicesManager.findServiceBy(selectedService);
selectedService.getId(), registeredService.getName());
//获取配置的登出路径
val logoutUrls = this.singleLogoutServiceLogoutUrlBuilder.determineLogoutUrl(registeredService,
selectedService);
if (logoutUrls == null || logoutUrls.isEmpty()) {
return new ArrayList<>(0);
}
return createLogoutRequests(ticketId, selectedService, registeredService, logoutUrls, ticketGrantingTicket);
}
BaseSingleLogoutServiceMessageHandler 类
protected Collection<LogoutRequest> createLogoutRequests(final String ticketId,
final WebApplicationService selectedService, final RegisteredService registeredService,
final Collection<URL> logoutUrls, final TicketGrantingTicket ticketGrantingTicket) {
return logoutUrls.stream().map(
// 获取到登出的应用地址,循环进行登出
url -> createLogoutRequest(ticketId, selectedService, registeredService, url, ticketGrantingTicket))
.filter(Objects::nonNull).collect(Collectors.toList());
}
BaseSingleLogoutServiceMessageHandler 类
private LogoutRequest createLogoutRequest(final String ticketId, final WebApplicationService selectedService,
final RegisteredService registeredService, final URL logoutUrl,
final TicketGrantingTicket ticketGrantingTicket) {
//build请求对象
val logoutRequest = DefaultSingleLogoutRequest.builder().ticketId(ticketId).service(selectedService)
.logoutUrl(logoutUrl).logoutType(registeredService.getLogoutType()).registeredService(registeredService)
.ticketGrantingTicket(ticketGrantingTicket).build();
final RegisteredService.LogoutType type = registeredService.getLogoutType() == null
? RegisteredService.LogoutType.BACK_CHANNEL
: registeredService.getLogoutType();
if (type == RegisteredService.LogoutType.BACK_CHANNEL) {
//cas一般是后端登出,即后台往应用地址发送请求
if (performBackChannelLogout(logoutRequest)) {
logoutRequest.setStatus(LogoutRequestStatus.SUCCESS);
} else {
logoutRequest.setStatus(LogoutRequestStatus.FAILURE);
}
} else {
selectedService, type);
logoutRequest.setStatus(LogoutRequestStatus.NOT_ATTEMPTED);
}
return logoutRequest;
}
public boolean performBackChannelLogout(final LogoutRequest request) {
try {
//封装请求参数,即post请求的表单信息
val logoutRequest = createSingleLogoutMessage(request);
final WebApplicationService logoutService = request.getService();
logoutService.setLoggedOutAlready(true);
final LogoutHttpMessage msg = new LogoutHttpMessage(request.getLogoutUrl(), logoutRequest.getPayload(),
this.asynchronous);
//向应用发出登出请求
return this.httpClient.sendMessageToEndPoint(msg);
} catch (final Exception e) {
log.error(e.getMessage(), e);
}
return false;
}
public SingleLogoutMessage createSingleLogoutMessage(final LogoutRequest logoutRequest) {
return this.logoutMessageBuilder.create(logoutRequest);
}
DefaultSingleLogoutMessageCreator 类
//客户端会根据这个xml判断是不是登出请求
private static final String LOGOUT_REQUEST_TEMPLATE = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"%s\" Version=\"2.0\" "
+ "IssueInstant=\"%s\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">%s"
+ "</saml:NameID><samlp:SessionIndex>%s</samlp:SessionIndex></samlp:LogoutRequest>";
@SuppressWarnings("rawtypes")
@Override
public SingleLogoutMessage create(final LogoutRequest request) {
val logoutRequest = String.format(LOGOUT_REQUEST_TEMPLATE, GENERATOR.getNewTicketId("LR"),
new ISOStandardDateFormat().getCurrentDateAndTime(),
request.getTicketGrantingTicket().getAuthentication().getPrincipal().getId(), request.getTicketId());
val builder = SingleLogoutMessage.builder();
if (request.getLogoutType() == RegisteredService.LogoutType.FRONT_CHANNEL) {
log.trace("Attempting to deflate the logout message [{}]", logoutRequest);
return builder.payload(CompressionUtils.deflate(logoutRequest)).build();
}
//返回请求体
return builder.payload(logoutRequest).build();
}
源码分析client端cas源码5.3.3
客户端配置登出拦截器,一般要放在第一个。
@Bean
public FilterRegistrationBean<SingleSignOutFilter> filterSingleRegistration() {
FilterRegistrationBean<SingleSignOutFilter> registration = new FilterRegistrationBean<SingleSignOutFilter>();
registration.setFilter(new SingleSignOutFilter());
Map<String,String> initParameters = new HashMap<>();
initParameters.put("casServerUrlPrefix",CAS_SERVER_URL_PREFIX );
registration.setInitParameters(initParameters);
//set mapping url
registration.addUrlPatterns("/*");
//set loading sequence
registration.setOrder(1);
return registration;
}
当收到server端发送的请求,会进入SingleSignOutFilter拦截器,看代码
SingleSignOutFilter 类
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
if (!this.handlerInitialized.getAndSet(true)) {
HANDLER.init();
}
//拦截器处理请求
if (HANDLER.process(request, response)) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
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;
}
private boolean isLogoutRequest(final HttpServletRequest request) {
if ("POST".equalsIgnoreCase(request.getMethod())) {
return !isMultipartRequest(request)
//通过这个判断,就是看看请求参数有没有默认值logoutRequest,参见server端有个xml:"<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"%s\" Version=\"2.0\" "
// + "IssueInstant=\"%s\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">%s"
// + "</saml:NameID><samlp:SessionIndex>%s</samlp:SessionIndex></samlp:LogoutRequest>"
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
this.safeParameters));
}
if ("GET".equalsIgnoreCase(request.getMethod())) {
return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters));
}
return false;
}
如果拦截器拦截到请求是登出请求,就把客户端sesssion失效
private void destroySession(final HttpServletRequest request) {
String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
if (CommonUtils.isBlank(logoutMessage)) {
return;
}
if (!logoutMessage.contains("SessionIndex")) {
logoutMessage = uncompressLogoutMessage(logoutMessage);
}
logger.trace("Logout request:\n{}", logoutMessage);
//从xml中获取应用的st
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
if (CommonUtils.isNotBlank(token)) {
//客户端移除st和session的关系
final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
if (session != null) {
try {
// session失效,客户端登出
session.invalidate();
} catch (final IllegalStateException e) {
logger.debug("Error invalidating session.", e);
}
this.logoutStrategy.logout(request);
}
}
}