SSO單點登錄------源碼分析

4 源碼解析

4.1 Server源碼解析

Cas server端採用Spring WebFlow來進行流程控制,因此本文以系統webflow文件爲切入點,對流程相關源碼進行分析。Cas系統的webflow文件位於WEB-INF/webflow目錄下,分爲登陸流程和登出流程。

4.1.1 登陸流程解析

4.1.1.1 訪問接入Cas系統的應用系統Client1

登陸流程配置文件爲login-webflow.xm。
瀏覽器首次訪問配置了單點登錄的應用系統時(http://www.client1.com/index),Client1會將請求重定向到cas系統
(http://www.casserver.com/serviceValidate?service=http://www.client1.com/index)
cas系統接收到瀏覽器發來的請求,整個登錄流程從此處開始,流程初始化。
WEB-INF/login-webflow.xml部分代碼:

 <on-start>
        <evaluate expression="initialFlowSetupAction"/>
    </on-start>

初始化部分會調用InitialFlowSetupAction類的doExecute方法,如果有特殊需求,可以在此方法中增加相應的邏輯。
InitialFlowSetupAction的doExecute方法:

@Override
    protected Event doExecute(final RequestContext context) throws Exception {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);

        final String contextPath = context.getExternalContext().getContextPath();
        final String cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/";

        if (StringUtils.isBlank(warnCookieGenerator.getCookiePath())) {
            logger.info("Setting path for cookies for warn cookie generator to: {} ", cookiePath);
            this.warnCookieGenerator.setCookiePath(cookiePath);
        } else {
            logger.debug("Warning cookie path is set to {} and path {}", warnCookieGenerator.getCookieDomain(),
                    warnCookieGenerator.getCookiePath());
        }
        if (StringUtils.isBlank(ticketGrantingTicketCookieGenerator.getCookiePath())) {
            logger.info("Setting path for cookies for TGC cookie generator to: {} ", cookiePath);
            this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
        } else {
            logger.debug("TGC cookie path is set to {} and path {}", ticketGrantingTicketCookieGenerator.getCookieDomain(),
                    ticketGrantingTicketCookieGenerator.getCookiePath());
        }
//將TGT放在FlowScope作用域中 
        WebUtils.putTicketGrantingTicketInScopes(context,
                this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
//將warnCookieValue放在FlowScope作用域中
        WebUtils.putWarningCookie(context,
                Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
//獲取service參數
        final Service service = WebUtils.getService(this.argumentExtractors, context);


        if (service != null) {
            logger.debug("Placing service in context scope: [{}]", service.getId());

            final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
            if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) {
                logger.debug("Placing registered service [{}] with id [{}] in context scope",
                        registeredService.getServiceId(),
                        registeredService.getId());
                WebUtils.putRegisteredService(context, registeredService);

                final RegisteredServiceAccessStrategy accessStrategy = registeredService.getAccessStrategy();
                if (accessStrategy.getUnauthorizedRedirectUrl() != null) {
                    logger.debug("Placing registered service's unauthorized redirect url [{}] with id [{}]in context scope",
                            accessStrategy.getUnauthorizedRedirectUrl(),
                            registeredService.getServiceId());
                    WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl());
                }
            }
        } else if (!this.enableFlowOnAbsentServiceRequest) {
            logger.warn("No service authentication request is available at [{}]. CAS is configured to disable the flow.",
                    WebUtils.getHttpServletRequest(context).getRequestURL());
            throw new NoSuchFlowExecutionException(context.getFlowExecutionContext().getKey(),
                    new UnauthorizedServiceException("screen.service.required.message", "Service is required"));
        }
//將service放在FlowScope作用域中 
        WebUtils.putService(context, service);
        return result("success");
    }

InitialFlowSetupAction的doExecute要做的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域中,以便在登錄流程中的state中進行判斷。初始化完成後,登錄流程流轉到第一個state(ticketGrantingTicketExistsCheck)。

<action-state id="ticketGrantingTicketCheck">
        <evaluate expression="ticketGrantingTicketCheckAction"/>
        <transition on="notExists" to="gatewayRequestCheck"/>
        <transition on="invalid" to="terminateSession"/>
        <transition on="valid" to="hasServiceCheck"/>
    </action-state>

ticketGrantingTicketCheckAction的doExecute方法判斷request的Cookie中是否攜帶有效的TGT,第一次訪問時沒有攜帶TGT,流程跳轉到gatewayRequestCheck。

<decision-state id="gatewayRequestCheck">
        <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null"
            then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck"/>
    </decision-state>

因爲初始化時,儘管把service保存在了FlowScope作用域中,但request中的參數gateway不存在,登錄流程流轉到第三個state(serviceAuthorizationCheck)。

 <action-state id="serviceAuthorizationCheck">
        <evaluate expression="serviceAuthorizationCheck"/>
        <transition to="initializeLogin"/>
    </action-state>

ServiceAuthorizationCheck的doExecute方法,要做的就是判斷FlowScope作用域中是否存在service,如果service存在,查找service的註冊信息。登錄流程流轉到第四個state(generateLoginTicket)。

  <action-state id="initializeLogin">
        <evaluate expression="'success'"/>
        <transition on="success" to="viewLoginForm"/>
    </action-state>

initializeLogin不做判斷,存在只是爲了兼容舊cas版本。直接跳轉到viewLoginForm。

<view-state id="viewLoginForm" view="casLoginView" model="credential">
        <binder>
            <binding property="username" required="true"/>
            <binding property="password" required="true"/>
        </binder>
        <on-entry>
            <set name="viewScope.commandName" value="'credential'"/>
        </on-entry>
        <transition on="submit" bind="true" validate="true" to="realSubmit"/>
    </view-state>

此時流轉到CAS單點登錄服務器端的登錄頁面casLoginView.jsp。

<action-state id="realSubmit">
        <evaluate
                expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)"/>
        <transition on="warn" to="warn"/>
        <transition on="success" to="sendTicketGrantingTicket"/>
        <transition on="successWithWarnings" to="showMessages"/>
        <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
        <transition on="error" to="initializeLogin"/>
    </action-state>

用戶在登錄頁面輸入賬號密碼提交後,流程走到realSumit。
authenticationViaFormAction類的submit()對用戶提交的認證信息進行驗證。

public final Event submit(final RequestContext context, final Credential credential,
                              final MessageContext messageContext)  {
        //判斷是否是已登錄過,請求ST的
        if (isRequestAskingForServiceTicket(context)) {
              //如果已登錄,則生成ST
return grantServiceTicket(context, credential);
        }
       //未登陸過,生成TGT
        return createTicketGrantingTicket(context, credential, messageContext);
    }

驗證成功則跳轉到sendTicketGrantingTicket。

<action-state id="sendTicketGrantingTicket">
        <evaluate expression="sendTicketGrantingTicketAction"/>
        <transition to="serviceCheck"/>
    </action-state>

接着跳轉到serviceCheck

<decision-state id="serviceCheck">
        <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess"/>
    </decision-state>

判斷是否是由應用頁面跳轉到登錄頁面登陸的,如果是,則跳轉到generateServiceTicket,不是則跳轉到viewGenericLoginSuccess。
此處我們跳轉到generateServiceTicket

 <action-state id="generateServiceTicket">
        <evaluate expression="generateServiceTicketAction"/>
        <transition on="success" to="warn"/>
        <transition on="unregisteredService" to="viewGenericLoginSuccess"/>
        <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
        <transition on="error" to="initializeLogin"/>
        <transition on="gateway" to="gatewayServicesManagementCheck"/>
    </action-state>

generateServiceTicketAction類的doExecute方法生成ST,並跳轉到warn

 <decision-state id="warn">
        <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/>
    </decision-state>

跳轉到redirect

 <action-state id="redirect">
        <evaluate expression="flowScope.service.getResponse(requestScope.serviceTicketId)"
                  result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response"/>
        <transition to="postRedirectDecision"/>
</action-state>

    <decision-state id="postRedirectDecision">
        <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView"/>
</decision-state>

    <end-state id="redirectView" view="externalRedirect:#{requestScope.response.url}"/>

最終返回給瀏覽器跳轉回Client1的響應。

4.1.1.2 訪問接入Cas系統的應用系統Client2

訪問Client1並登陸之後,訪問Client2,與訪問Client1一樣,先經過initialFlowSetupAction。
隨後登錄流程流轉到第一個state(ticketGrantingTicketExistsCheck)。

<action-state id="ticketGrantingTicketCheck">
        <evaluate expression="ticketGrantingTicketCheckAction"/>
        <transition on="notExists" to="gatewayRequestCheck"/>
        <transition on="invalid" to="terminateSession"/>
        <transition on="valid" to="hasServiceCheck"/>
    </action-state>

因爲已經登陸過,擁有請求的Cookie中存在有效的TGT,於是流程跳轉到hasServiceCheck。

 <decision-state id="hasServiceCheck">
        <if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess"/>
    </decision-state>

判斷是否是由應用頁面跳轉到登錄頁面登陸的,如果是,則跳轉到generateServiceTicket,不是則跳轉到viewGenericLoginSuccess。
此處跳轉到renewRequestCheck。

 <decision-state id="renewRequestCheck">
        <if test="requestParameters.renew != '' and requestParameters.renew != null" then="serviceAuthorizationCheck"
            else="generateServiceTicket"/>
    </decision-state>

request中不存在renew,登錄流程流轉到第四個state(generateServiceTicket)。

 <action-state id="generateServiceTicket">
        <evaluate expression="generateServiceTicketAction"/>
        <transition on="success" to="warn"/>
        <transition on="unregisteredService" to="viewGenericLoginSuccess"/>
        <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
        <transition on="error" to="initializeLogin"/>
        <transition on="gateway" to="gatewayServicesManagementCheck"/>
    </action-state>

後續的流轉與應用系統webapp1相同,請參考前面webapp1的流轉。

4.1.2 登出流程解析

登出的流程定義在logout-webflow.xml中。
首先訪問登出接口/logout,流程跳轉到terminateSession

 <action-state id="terminateSession">
    <evaluate expression="terminateSessionAction.terminate(flowRequestContext)" />
    <transition to="doLogout" />
  </action-state>

登出的方法主要調用路徑如下:

TerminateSessionAction.terminate() 
--> CentralAuthenticationServiceImpl.destroyTicketGrantingTicket()
銷燬TGT的方法
--> LogoutManagerImpl.performLogout() 
執行登出的方法,在該方法中向每個訪問過的應用系統發送登出請求, 應用系統收到請求會銷燬與用戶的session
--> handleLogoutForSloService()
嚮應用系統發送登出請求的方法
--> performBackChannelLogout()   發送登出請求

terminateSessionAction.terminate()執行完畢之後,流程跳轉到doLogout

 <action-state id="doLogout">
    <evaluate expression="logoutAction" />
    <transition on="finish" to="finishLogout" />
    <transition on="front" to="frontLogout" />
  </action-state>

 <decision-state id="finishLogout">
    <if test="flowScope.logoutRedirectUrl != null" then="redirectView" else="logoutView" />
  </decision-state>

  <end-state id="logoutView" view="externalRedirect:casLoginView" />

最終跳轉到登錄頁面。

4.2 Client端源碼解析

Cas client應用系統端通過幾個Filter來實現登陸跳轉和登出等功能。
下面以在web.xml中配置的幾個Filter順序來進行分析 。

4.2.1 SingleSignOutFilter

org.jasig.cas.client.session.SingleSignOutFilter是處理登出請求的Filter。該Filter判斷是否是Cas Server端發過來的登出請求,如果是登出請求,則根據請求中的logoutMessage清除對應的Session。
登出請求的主要調用路徑如下:

SingleSignOutFilter.doFilter()
--> SingleSignOutHandler.process()
--> destroySession(request)
--> session.invalidate();

4.2.2 AuthenticationFilter

org.jasig.cas.client.authentication.AuthenticationFilter是驗證請求是否登陸過的Filter。

public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        // 判斷該請求是否不需要驗證,如果不需要,則跳轉到下一個Filter
if(this.isRequestUrlExcluded(request)) {
            this.logger.debug("Request is ignored.");
            filterChain.doFilter(request, response);
        } else {
            HttpSession session = request.getSession(false);
           //從session中獲取名爲"_const_cas_assertion_"的Assertion 
 Assertion assertion = session != null?(Assertion)session.getAttribute("_const_cas_assertion_"):null;
           //如果存在,則說明已經登錄,本過濾器處理完成,處理下個過濾器 
          if(assertion != null) {
                filterChain.doFilter(request, response);
            } else {
//生成serviceUrl  
                String serviceUrl = this.constructServiceUrl(request, response);
//從request中獲取ST 
                String ticket = this.retrieveTicketFromRequest(request);
                boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
                //如果ticket不爲空,本過濾器處理完成,處理下個過濾器 
if(!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
                    this.logger.debug("no ticket and no assertion found");
                    String modifiedServiceUrl;
                    if(this.gateway) {
                        this.logger.debug("setting gateway attribute in session");
                        modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
                    } else {
                        modifiedServiceUrl = serviceUrl;
                    }

                    this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
                  //生成重定向URL 
                    String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
                    this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
                    String reqType = request.getHeader("X-Requested-With");
                    //如果是異步請求,則返回410狀態碼給前端
                    if("XMLHttpRequest".equalsIgnoreCase(reqType)) {
                        String json = "{\"flag\":0,\"error\":401,\"data\":{}}";
                        response.getWriter().write(json);
                    } else {
                    //跳轉到CAS服務器的登錄頁面 
                        this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
                    }

                } else {
                    filterChain.doFilter(request, response);
                }
            }
        }
    }

當我們從瀏覽器訪問配置了單點登錄的應用系統時(http://www.client1.com/index),由於集成了CAS單點登錄客戶端,此時進入到第一個過濾器AuthenticationFilter(不考慮其他非單點登錄的過濾器),執行以下操作:
1 從session中獲取名爲“const_cas_assertion”的assertion對象,判斷assertion是否存在,如果存在,說明已經登錄,執行下一個過濾器。如果不存在,執行第2步。
2 生成serviceUrl(http://www.client1.com/index),從request中獲取票據參數ticket,判斷ticket是否爲空,如果不爲空執行下一個過濾器。如果爲空,執行第3步。
3 生成重定向URL,如:
http://www.casserver.com/login?service=http://www.client1.com/index
4 跳轉到單點登錄服務器,顯示登錄頁面,此時第一個過濾器執行完成。

4.2.3 ticketValidationFilter

org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter爲驗證Service Ticket的Filter。
Cas20ProxyReceivingTicketValidationFilter父類AbstractTicketValidationFilter中的doFilter方法:

public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if(this.preFilter(servletRequest, servletResponse, filterChain)) {
            HttpServletRequest request = (HttpServletRequest)servletRequest;
            HttpServletResponse response = (HttpServletResponse)servletResponse;
            //從request中獲取參數 
            String ticket = this.retrieveTicketFromRequest(request);
            //ticket不爲空,驗證ticket,否則本過濾器處理完成,處理下個過濾器
            if(CommonUtils.isNotBlank(ticket)) {
                this.logger.debug("Attempting to validate ticket: {}", ticket);

                try {
//驗證ticket併產生Assertion對象,錯誤拋出TicketValidationException異常 
  Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
             this.logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
                   //給request設置assertion
                   request.setAttribute("_const_cas_assertion_", assertion);
                   //給session設置assertion  
                   if(this.useSession) {
                        request.getSession().setAttribute("_const_cas_assertion_", assertion);
                    }

                    this.onSuccessfulValidation(request, response, assertion);
                    if(this.redirectAfterValidation) {
                        this.logger.debug("Redirecting after successful ticket validation.");
                        response.sendRedirect(this.constructServiceUrl(request, response));
                        return;
                    }
                } catch (TicketValidationException var8) {
                    this.logger.debug(var8.getMessage(), var8);
                    this.onFailedValidation(request, response);
                    if(this.exceptionOnValidationFailure) {
                        throw new ServletException(var8);
                    }

                    response.sendError(403, var8.getMessage());
                    return;
                }
            }

            filterChain.doFilter(request, response);
        }
    }

假設當執行完第一個過濾器後,跳轉到CAS服務器端的登錄頁面,輸入用戶名和密碼,驗證通過後。CAS服務器端會生成ticket,並將ticket作爲重新跳轉到應用系統的參數(http://www.client1.com/index?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)。此時又進入第一個過濾器AuthenticationFilter,由於存在ticket參數,進入到第二個過濾器TicketValidationFilter,執行以下操作:
1 從request獲取ticket參數,如果ticket爲空,繼續處理下一個過濾器。如果參數不爲空,驗證ticket參數的合法性。
2 驗證ticket,TicketValidationFilter的validate方法通過httpClient訪問CAS服務器端(http://www.casserver.com/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://www.client1.com/index)驗證ticket是否正確,並返回assertion對象。如果驗證失敗,拋出異常,跳轉到錯誤頁面。如果驗證成功,session會以"const_cas_assertion"的名稱保存assertion對象,繼續處理下一個過濾器。

4.2.4 HttpServletRequestWrapperFilter

org.jasig.cas.client.util.HttpServletRequestWrapperFilter對HttpServletRequest對象再包裝一次,讓其支持getUserPrincipal,getRemoteUser方法來取得登錄的用戶信息。

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 
 //從Session或者request中取得AttributePrincipal,其實Assertion的一個principal屬性 
AttributePrincipal principal = this.retrievePrincipalFromSessionOrRequest(servletRequest);
//對request進行包裝,並處理後面的過濾器,使其後面的過濾器或者servlet能夠在request.getRemoteUser()或者request.getUserPrincipal()取得用戶信息  
filterChain.doFilter(newHttpServletRequestWrapperFilter.CasHttpServletRequestWrapper((HttpServletRequest)servletRequest, principal), servletResponse); }

5 常見問題

6 其他

6.1 Gradle相關

6.1.1 Gradle 打包實現生產環境與測試環境配置分離

在build.gradle中加入以下代碼

#默認情況下爲ent-dev
def env = System.getProperty("profile") ?: "ent-dev"
sourceSets {
    main {
        resources {
            srcDirs = ["src/main/resources", "src/main/$env"]
        }
    }
}

在/src/main目錄下建立各個環境目錄,如 ent-dev、ent-prod等。
對於cas系統,配置參數都在cas.properties中,可以將cas.properties放入各個環境目錄中, 並修改讀取cas.properties路徑。
修改WEB-INF/spring-configuration目錄中的propertyFileConfigurer.xml

 <util:properties id="casProperties" location="${cas.properties.config.location:/WEB-INF/cas.properties}"/>

修改爲

<util:properties id="casProperties" location="${cas.properties.config.location:classpath:/cas.properties}"/>

build生產環境時可使用命令:gradle build -D profile=ent-prod

6.2 調用263和LDAP驗證用戶名密碼步驟

6.2.1 在項目中添加依賴

在項目build.gradle中加入以下內容,如果是Maven項目,則在pom.xml文件中加入maven格式依賴。

shangdeCommonSdfVersion=0.1.0.5-ENT-SNAPSHOT
compile group: 'com.xxx.common', name: 'sdf-common-util', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-web', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-auth-web', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-sys', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-authentication-263', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-auth', version: shangdeCommonSdfVersion

6.2.2 在properties文件中添加以下內容

263.domain=xyz.com
263.account=xyz.com
263.key=Zs54D6jo#
263.webServiceUrl=http://macom.263.net/axis/xmapi
ldap.url=ldap://172.16.117.215:389

6.2.3 調用驗證方法

@Autowired
private SdfPasswordValidatorImpl sdfPasswordValidatorImpl;
// 返回值爲true及驗證成功
boolean isValidUser = sdfPasswordValidatorImpl.authenticate(username,password);



作者:Ferrari1001
鏈接:https://www.jianshu.com/p/3dcaaa12e976
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發佈了7 篇原創文章 · 獲贊 12 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章