CAS單點登錄源碼解析之【單點登出】

前期準備

已經搭建好了集成了CAS客戶端的應用系統和CAS服務器

1.應用系統webapp(http://127.0.0.1:8090/webapp/main.do)

2.CAS單點登錄服務器端(http://127.0.0.1:8081/cas-server/)

        本次討論包括CAS單點登錄客戶端的部分源碼,以及在此基礎上進行單點登出二次開發,因此需要修改部分CAS客戶端的源碼,源碼部分的修改在下面進行討論。關於CAS客戶端和服務器端的源碼分析,請參考另外兩篇文章

CAS客戶端:http://blog.csdn.net/dovejing/article/details/44426547

CAS服務器端:http://blog.csdn.net/dovejing/article/details/44523545

應用系統web.xml部分代碼

<listener>
	<listener-class>com.master.client.listener.SingleSignOutHttpSessionListener</listener-class>
</listener>
<filter>
	<filter-name>CAS Single Sign Out Filter</filter-name>
	<filter-class>com.master.client.filter.SingleSignOutFilter</filter-class>
</filter>

應用系統登出方法logout

public String logout(HttpSession session, HttpServletResponse response,
		HttpServletRequest request) {
	try {
		Properties conf = PropertiesUtil.getConfigProperties();
		String service = ssoProperties.getProperty("service");
		String logoutUrl = ssoProperties.getProperty("casServerLogoutUrl");
		String serverName = ssoProperties.getProperty("serverName");
		//生成CAS服務器端的登出URL
		String serviceUrl = CommonUtils.constructServiceUrl(request, response, service, serverName, "ticket", true);

		response.sendRedirect(logoutUrl + "?service=" + URLEncoder.encode(serviceUrl, "utf-8"));
	} catch (URISyntaxException e) {
		e.printStackTrace();
	} catch (UnsupportedEncodingException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
		
	return null;
}

當我們在應用系統中,執行註銷或退出操作時,會執行logout方法。應用系統登出方法logout要做是獲取單點登錄的配置文件(參考http://blog.csdn.net/dovejing/article/details/44426547),獲取service、logoutUrl和serverName參數。生成CAS服務器端的登出URL(http://127.0.0.1:8081/cas-server/logout?service=http://127.0.0.1:8090/webapp/main.do),response重定向到CAS服務器端的登出URL。

CAS服務器端cas-server.xml部分代碼

<bean id="handlerMappingC" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
	<property name="mappings">
		<props>
			<prop key="/logout">logoutController</prop><!-- 登出 -->
			<prop key="/serviceValidate">serviceValidateController</prop>
			<prop key="/validate">legacyValidateController</prop>
			<prop key="/proxy">proxyController</prop>
			<prop key="/proxyValidate">proxyValidateController</prop>
			<prop key="/samlValidate">samlValidateController</prop>
			<prop key="/services/add.html">addRegisteredServiceSimpleFormController</prop>
			<prop key="/services/edit.html">editRegisteredServiceSimpleFormController</prop>
			<prop key="/services/loggedOut.html">serviceLogoutViewController</prop>
			<prop key="/services/viewStatistics.html">viewStatisticsController</prop>
			<prop key="/services/*">manageRegisteredServicesMultiActionController</prop>
			<prop key="/openid/*">openIdProviderController</prop>
			<prop key="/authorizationFailure.html">passThroughController</prop>
			<prop key="/403.html">passThroughController</prop>
			<prop key="/status">healthCheckController</prop>
			<prop key="/error">extErrorController</prop>
		</props>
	</property>
	<property name="alwaysUseFullPath" value="true" />
</bean>
logoutController配置信息
<bean id="logoutController" class="org.jasig.cas.web.LogoutController"
	p:centralAuthenticationService-ref="centralAuthenticationService"
	p:logoutView="casLogoutView" p:warnCookieGenerator-ref="warnCookieGenerator"
	p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"
	p:servicesManager-ref="servicesManager" p:followServiceRedirects="${cas.logout.followServiceRedirects:true}" />

根據配置信息,當CAS單點登錄服務器端截獲http://127.0.0.1:8081/cas-server/logout?service=http://127.0.0.1:8090/webapp/main.do鏈接時,會進入到LogoutController的handleRequestInternal方法。

LogoutController的handleRequestInternal方法

protected ModelAndView handleRequestInternal(
	final HttpServletRequest request, final HttpServletResponse response)
	throws Exception {
	//獲取TGT
	final String ticketGrantingTicketId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
	//獲取service
	final String service = request.getParameter("service");
        
	//如果TGT不爲空
	if (ticketGrantingTicketId != null) {
		//銷燬TGT
		this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
		//銷燬TGTcookie
		this.ticketGrantingTicketCookieGenerator.removeCookie(response);
		//銷燬warnCookieValue
		this.warnCookieGenerator.removeCookie(response);
	}

	//如果service不會空
	if (this.followServiceRedirects && service != null) {
		final RegisteredService rService = this.servicesManager.findServiceBy(new SimpleWebApplicationServiceImpl(service));

		if (rService != null && rService.isEnabled()) {
			//跳轉到service
			return new ModelAndView(new RedirectView(service));
		}
	}
	
	return new ModelAndView(this.logoutView);
}
LogoutController的handleRequestInternal要做是從request的cookies中獲取TGC,從request中獲取service,同時銷燬服務器緩存的TGT和response中的TGC。並跳轉到應用系統的service(http://127.0.0.1:8090/webapp/main.do)頁面。

CentralAuthenticationServiceImpl的destroyTicketGrantingTicket方法

public void destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
    Assert.notNull(ticketGrantingTicketId);

    if (log.isDebugEnabled()) {
            log.debug("Removing ticket [" + ticketGrantingTicketId + "] from registry.");
    }
    final TicketGrantingTicket ticket = (TicketGrantingTicket) this.ticketRegistry.getTicket(ticketGrantingTicketId, 
		TicketGrantingTicket.class);

    if (ticket == null) {
        return;
    }

    if (log.isDebugEnabled()) {
        log.debug("Ticket found.  Expiring and then deleting.");
    }
    ticket.expire();
    this.ticketRegistry.deleteTicket(ticketGrantingTicketId);
}
之前在CAS服務器端創建TGT的時候,我們把TGT放到了ticketRegistry對象中,所以此時需要對ticketRegistry中的TGT進行操作,從ticketRegistry對象中獲取TGT,並執行TGT的expire方法,設置TGT的屬性爲過期,expire方法同會時執行logOutOfServices方法。

TicketGrantingTicket的expire方法和logOutOfServices方法

public synchronized void expire() {
    this.expired = true;
    logOutOfServices();
}

private void logOutOfServices() {
    for (final Entry<String, Service> entry : this.services.entrySet()) {

        if (!entry.getValue().logOutOfService(entry.getKey())) {
            LOG.warn("Logout message not sent to [" + entry.getValue().getId() + "]; Continuing processing...");   
        }
    }
}
logOutOfServices方法會循環所有的Service(AbstractWebApplicationService實現類),並執行logOutOfService方法。

AbstractWebApplicationService的logOutOfServices方法

public synchronized boolean logOutOfService(final String sessionIdentifier) {
    if (this.loggedOutAlready) {
		return true;
    }

    LOG.debug("Sending logout request for: " + getId());

    final String logoutRequest = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\""
        + GENERATOR.getNewTicketId("LR")
        + "\" Version=\"2.0\" IssueInstant=\"" + SamlUtils.getCurrentDateAndTime()
        + "\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@</saml:NameID><samlp:SessionIndex>"
        + sessionIdentifier + "</samlp:SessionIndex></samlp:LogoutRequest>";
        
    this.loggedOutAlready = true;
        
    if (this.httpClient != null) {
        return this.httpClient.sendMessageToEndPoint(getOriginalUrl(), logoutRequest, true);
    }
        
    return false;
}

logOutOfService會構造logoutRequest對象,同時執行HttpClient的sendMessageToEndPoint方法訪問客戶端,此時,客戶端的過濾器會攔截這個請求,並對logoutRequest進行解析,獲取sessionIndex(sessionIdentifier),並根據sessionIndex銷燬session信息。

SingleSignOutFilter的doFilter方法

public void doFilter(ServletRequest servletRequest,
		ServletResponse servletResponse, FilterChain filterChain)
		throws IOException, ServletException {
	HttpServletRequest request = (HttpServletRequest) servletRequest;
	//request是否有ticket
	if (handler.isTokenRequest(request)) {
		//sessionMappingStorage記錄session。
		handler.recordSession(request);
	} else {
		//request是否有logoutRequest
		if (handler.isLogoutRequest(request)) {
			//sessionMappingStorag註銷session。
			handler.destroySession(request);
			//不執行後續過濾器 
			return;
		}
		this.log.trace("Ignoring URI " + request.getRequestURI());
	}

	filterChain.doFilter(servletRequest, servletResponse);
}

SingleSignOutFilter的doFilter方法,要做的是如果request有ticket參數,則記錄session到sessionMappingStorage中,執行後續過濾器。如果request有logoutRequest參數,則從sessionMappingStorage銷燬session,不執行後續過濾器。

SingleSignOutHttpSessionListener類源碼

private SessionMappingStorage sessionMappingStorage;

public void sessionCreated(final HttpSessionEvent event) {
	// nothing to do at the moment
}
public void sessionDestroyed(final HttpSessionEvent event) {
	if (sessionMappingStorage == null) {
		sessionMappingStorage = getSessionMappingStorage();
	}
	final HttpSession session = event.getSession();
	//從sessionMappingStorage刪除session
	sessionMappingStorage.removeBySessionById(session.getId());
}

protected static SessionMappingStorage getSessionMappingStorage() {
	return SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage();
}
當session銷燬時,會觸發SingleSignOutHttpSessionListener類(父類HttpSessionListener)的銷燬事件,此時,sessionDestroyed方法會從sessionMappingStorage中刪除session信息。完成之後,會繼續執行LogoutController的handleRequestInternal方法,並跳轉到應用系統的service(http://127.0.0.1:8090/webapp/main.do)頁面。

至此,CAS的單點登出操作流程已經完成,正常情況下會顯示CAS的登錄頁面。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章