CAS登出原理-源码分析

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);
            }
        }
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值