jasig CAS登陸驗證分析

jasig CAS登陸驗證分析:

以前文章講到了怎麼利用jasig CAS實現sso: html

http://my.oschina.net/indestiny/blog/200768 前端

本文對jasig CAS驗證過程作個簡單的分析,便於之後可以更好定製本身的CAS, 要了解CAS流程你須要知道spring,springmvc等知識,也要了解spring-webflow, 由於整個驗證流程都是由spring-webflow定製的,你能夠參考我轉載的一篇spring-webflow的文章: java

http://my.oschina.net/indestiny/blog/201988 web

ok, 就開始了。 spring

  • 先說說咱們未登陸狀態時:

重點就是服務器端的配置:WEB-INF/login-webflow.xml中,它定義了整個登陸流程,咱們先就分析其流程: 數據庫

<flow xmlns="http://www.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow
                          http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

    <var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
    <on-start>
        <evaluate expression="initialFlowSetupAction" />
    </on-start>

	<decision-state id="ticketGrantingTicketExistsCheck">
		<if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
	</decision-state>
    
	<decision-state id="gatewayRequestCheck">
		<if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null" then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck" />
	</decision-state>
	
	<decision-state id="hasServiceCheck">
		<if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" />
	</decision-state>
	
	<decision-state id="renewRequestCheck">
		<if test="requestParameters.renew != '' and requestParameters.renew != null" then="serviceAuthorizationCheck" else="generateServiceTicket" />
	</decision-state>

    <!-- Do a service authorization check early without the need to login first -->
    <action-state id="serviceAuthorizationCheck">
        <evaluate expression="serviceAuthorizationCheck"/>
        <transition to="generateLoginTicket"/>
    </action-state>
	
	<!-- 
		The "warn" action makes the determination of whether to redirect directly to the requested
		service or display the "confirmation" page to go back to the server.
	-->
	<decision-state id="warn">
		<if test="flowScope.warnCookieValue" then="showWarningView" else="redirect" />
	</decision-state>
	
	<!-- 
	<action-state id="startAuthenticate">
		<action bean="x509Check" />
		<transition on="success" to="sendTicketGrantingTicket" />
		<transition on="warn" to="warn" />
		<transition on="error" to="generateLoginTicket" />
	</action-state>
	 -->
    
    <!--
   		LPPE transitions begin here: You will also need to
   		move over the 'lppe-configuration.xml' file from the
   		'unused-spring-configuration' folder to the 'spring-configuration' folder
   		so CAS can pick up the definition for the bean 'passwordPolicyAction'.
   	-->
	<action-state id="passwordPolicyCheck">
		<evaluate expression="passwordPolicyAction" />
		<transition on="showWarning" to="passwordServiceCheck" />
		<transition on="success" to="sendTicketGrantingTicket" />
		<transition on="error" to="viewLoginForm" />
	</action-state>

	<action-state id="passwordServiceCheck">
		<evaluate expression="sendTicketGrantingTicketAction" />
		<transition to="passwordPostCheck" />
	</action-state>

	<decision-state id="passwordPostCheck">
		<if test="flowScope.service != null" then="warnPassRedirect" else="pwdWarningPostView" />
	</decision-state>

	<action-state id="warnPassRedirect">
		<evaluate expression="generateServiceTicketAction" />
		<transition on="success" to="pwdWarningPostView" />
		<transition on="error" to="generateLoginTicket" />
		<transition on="gateway" to="gatewayServicesManagementCheck" />
	</action-state>

	<end-state id="pwdWarningAbstractView">
		<on-entry>
			<set name="flowScope.passwordPolicyUrl" value="passwordPolicyAction.getPasswordPolicyUrl()" />
		</on-entry>
	</end-state>
	<end-state id="pwdWarningPostView" view="casWarnPassView" parent="#pwdWarningAbstractView" />
	<end-state id="casExpiredPassView" view="casExpiredPassView" parent="#pwdWarningAbstractView" />
	<end-state id="casMustChangePassView" view="casMustChangePassView" parent="#pwdWarningAbstractView" />
	<end-state id="casAccountDisabledView" view="casAccountDisabledView" />
	<end-state id="casAccountLockedView" view="casAccountLockedView" />
	<end-state id="casBadHoursView" view="casBadHoursView" />
	<end-state id="casBadWorkstationView" view="casBadWorkstationView" />
	<!-- LPPE transitions end here... -->
	
	<action-state id="generateLoginTicket">
        <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" />
		<transition on="generated" to="viewLoginForm" />
	</action-state>
    
	<view-state id="viewLoginForm" view="casLoginView" model="credentials">
        <binder>
            <binding property="username" />
            <binding property="password" />
        </binder>
        <on-entry>
            <set name="viewScope.commandName" value="'credentials'" />
        </on-entry>
		<transition on="submit" bind="true" validate="true" to="realSubmit">
            <evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
        </transition>
	</view-state>

  	<action-state id="realSubmit">
        <evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />
        <!--
	      To enable LPPE on the 'warn' replace the below transition with:
	      <transition on="warn" to="passwordPolicyCheck" />

	      CAS will attempt to transition to the 'warn' when there's a 'renew' parameter
	      and there exists a ticketGrantingId and a service for the incoming request.
	    -->
		<transition on="warn" to="warn" />
		<!--
	      To enable LPPE on the 'success' replace the below transition with:
	      <transition on="success" to="passwordPolicyCheck" />
	    -->
		<transition on="success" to="sendTicketGrantingTicket" />
		<transition on="error" to="generateLoginTicket" />
		<transition on="accountDisabled" to="casAccountDisabledView" />
	    <transition on="mustChangePassword" to="casMustChangePassView" />
	    <transition on="accountLocked" to="casAccountLockedView" />
	    <transition on="badHours" to="casBadHoursView" />
	    <transition on="badWorkstation" to="casBadWorkstationView" />
	    <transition on="passwordExpired" to="casExpiredPassView" />
	</action-state>
	
	<action-state id="sendTicketGrantingTicket">
        <evaluate expression="sendTicketGrantingTicketAction" />
		<transition to="serviceCheck" />
	</action-state>

	<decision-state id="serviceCheck">
		<if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess" />
	</decision-state>
	
	<action-state id="generateServiceTicket">
        <evaluate expression="generateServiceTicketAction" />
		<transition on="success" to ="warn" />
		<transition on="error" to="generateLoginTicket" />
		<transition on="gateway" to="gatewayServicesManagementCheck" />
	</action-state>

    <action-state id="gatewayServicesManagementCheck">
        <evaluate expression="gatewayServicesManagementCheck" />
        <transition on="success" to="redirect" />
    </action-state>

    <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>

	<!-- 
		the "viewGenericLogin" is the end state for when a user attempts to login without coming directly from a service.
		They have only initialized their single-sign on session.
	-->
	<end-state id="viewGenericLoginSuccess" view="casLoginGenericSuccessView" />

	<!-- 
		The "showWarningView" end state is the end state for when the user has requested privacy settings (to be "warned") to be turned on.  It delegates to a 
		view defines in default_views.properties that display the "Please click here to go to the service." message.
	-->
	<end-state id="showWarningView" view="casLoginConfirmView" />

    <end-state id="postView" view="postResponseView">
        <on-entry>
            <set name="requestScope.parameters" value="requestScope.response.attributes" />
            <set name="requestScope.originalUrl" value="flowScope.service.id" />
        </on-entry>
    </end-state>

	<!-- 
		The "redirect" end state allows CAS to properly end the workflow while still redirecting
		the user back to the service required.
	-->
	<end-state id="redirectView" view="externalRedirect:${requestScope.response.url}" />
	
	<end-state id="viewServiceErrorView" view="viewServiceErrorView" />
    
    <end-state id="viewServiceSsoErrorView" view="viewServiceSsoErrorView" />

	<global-transitions>
        <!-- CAS-1023 This one is simple - redirects to a login page (same as renew) when 'ssoEnabled' flag is unchecked
             instead of showing an intermediate unauthorized view with a link to login page -->
        <transition to="viewLoginForm" on-exception="org.jasig.cas.services.UnauthorizedSsoServiceException"/>
        <transition to="viewServiceErrorView" on-exception="org.springframework.webflow.execution.repository.NoSuchFlowExecutionException" />
		<transition to="viewServiceErrorView" on-exception="org.jasig.cas.services.UnauthorizedServiceException" />
	</global-transitions>
</flow>
首先設置了一個變量 credentials來保存用戶名及密碼信息:

<var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
在該flow執行一開始,作一次初始化:

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

對應其配置在/WEB-INF/cas-servlet.xml中: express

<bean id="initialFlowSetupAction" class="org.jasig.cas.web.flow.InitialFlowSetupAction"
        p:argumentExtractors-ref="argumentExtractors"
        p:warnCookieGenerator-ref="warnCookieGenerator"
        p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"/>

其中argumentExtractors配置/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml中: 瀏覽器

<bean
    id="casArgumentExtractor"
    class="org.jasig.cas.web.support.CasArgumentExtractor"
    p:httpClient-ref="noRedirectHttpClient"
    p:disableSingleSignOut="${slo.callbacks.disabled:false}" />

<bean id="samlArgumentExtractor" class="org.jasig.cas.web.support.SamlArgumentExtractor"
    p:httpClient-ref="noRedirectHttpClient"
    p:disableSingleSignOut="${slo.callbacks.disabled:false}" />
 	
 <util:list id="argumentExtractors">
    <ref bean="casArgumentExtractor" />
    <ref bean="samlArgumentExtractor" />
 </util:list>

其中ticketGrantingTicketCookieGenerator配置在/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml: 服務器

<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
	p:cookieSecure="true"
	p:cookieMaxAge="-1"
	p:cookieName="CASTGC"
	p:cookiePath="/cas" />

其中warnCookieGenerator的配置在/WEB-INF/spring-configuration/warnCookieGenerator.xml: cookie

<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
	p:cookieSecure="true"
	p:cookieMaxAge="-1"
	p:cookieName="CASPRIVACY"
	p:cookiePath="/cas" />
對應會調用InitialFlowSetupAction的doExecute方法:

protected Event doExecute(final RequestContext context) throws Exception {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
        if (!this.pathPopulated) {
            final String contextPath = context.getExternalContext().getContextPath();
            final String cookiePath = StringUtils.hasText(contextPath) ? contextPath + "/" : "/";
            logger.info("Setting path for cookies to: "
                + cookiePath);
            this.warnCookieGenerator.setCookiePath(cookiePath);
            this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
            this.pathPopulated = true;
        }
        context.getFlowScope().put(
            "ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
        context.getFlowScope().put(
            "warnCookieValue",
            Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));

     final Service service = WebUtils.getService(this.argumentExtractors, context);
     context.getFlowScope().put("service", service);
     return result("success");
}

講完初始化flow配置,看看第一個state(ticketGrantingTicketExistsCheck), 當第一次登陸cas時(https://cas_server:8443/cas/login), 沒有ticketGrantingTicketId, 因此會留向gatewayRequestCheck state:

<decision-state id="ticketGrantingTicketExistsCheck">
    <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
</decision-state>
看gatewayRequestCheck state,第一次service也是爲null, 因此流向serviceAuthorizationCheck state:
<decision-state id="gatewayRequestCheck">
    <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null" then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck" />
</decision-state>
繼續看serviceAuthorizationCheck state, 其會先調用 org.jasig.cas.web.flow.ServiceAuthorizationCheck的doExecute方法,以後流向generateLoginTicket,生成ticket:
<action-state id="serviceAuthorizationCheck">
    <evaluate expression="serviceAuthorizationCheck"/>
    <transition to="generateLoginTicket"/>
</action-state>
看generateLoginTicket state, 調用generateLoginTicketAction.generate方法來生成ticket,返回給客戶端:
<action-state id="generateLoginTicket">
    <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" />
    <transition on="generated" to="viewLoginForm" />
</action-state>
從CAS server debug信息和個人請求信息來看,server先生成這個ticket,返回給瀏覽器,當咱們登陸時,會帶上這個ticket:

我登陸時請求信息:

仍是看看ticket怎麼生成的吧,generateLoginTicketAction bean:

<bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction"
        p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator"/>
/WEB-INF/spring-configuration/uniqueIdGenerators.xml定義了不少Generator, 好比上面的LoginTicketUniqueIdGenerator:
<bean id="loginTicketUniqueIdGenerator" class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator">
    <constructor-arg index="0" type="int" value="30" />
</bean>
接着看GenerateLoginTicketAction的generate方法:
public class GenerateLoginTicketAction {
    /** 3.5.1 - Login tickets SHOULD begin with characters "LT-" */
    private static final String PREFIX = "LT";
    @NotNull
    private UniqueTicketIdGenerator ticketIdGenerator;

    public final String generate(final RequestContext context) {
        final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);//調用generator生成
        this.logger.debug("Generated login ticket " + loginTicket);
        WebUtils.putLoginTicket(context, loginTicket);//最終放到flowScope中
        return "generated";
    }
    ...
}

生成以後,就流向viewLoginForm state,其view未casLoginView,對應就是/WEB-INF/jsp/ui/default/casLoginView.jsp了:

<view-state id="viewLoginForm" view="casLoginView" model="credentials">
    <binder><!-- 綁定html form表單中的用戶名及密碼 -->
        <binding property="username" />
        <binding property="password" />
    </binder>
    <on-entry>
        <set name="viewScope.commandName" value="'credentials'" />
    </on-entry>
    <transition on="submit" bind="true" validate="true" to="realSubmit">
        <evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
    </transition>
</view-state>

因而就看到了CAS的登陸界面:

對應的html表單內容大概是:

<form id="fm1" class="fm-v clearfix" action="/cas/login" method="post">
    <h2>請輸入您的用戶名和密碼.</h2>
    <div class="row fl-controls-left">
        <label for="username" class="fl-label">用戶名:</label>					
        <input id="username" name="username" class="required" tabindex="1" accesskey="n" type="text" value                ="" size="25" autocomplete="false"/>
    </div>
    <div class="row fl-controls-left">
        <label for="password" class="fl-label">密 碼:</label>
        <input id="password" name="password" class="required" tabindex="2" accesskey="p" type="password" v                alue="" size="25" autocomplete="off"/>
    </div>
    <div class="row check">
        <input id="warn" name="warn" value="true" tabindex="3" accesskey="w" type="checkbox" />
        <label for="warn">轉向其餘站點前提示我。</label>
    </div>
    <div class="row btn-row">
        <input type="hidden" name="lt" value="LT-5-rCdFkUxqSVKWTpzNgn2hLoZe9Fq0I2" /><!--生成的ticket-->
        <input type="hidden" name="execution" value="e1s1" />
	 <input type="hidden" name="_eventId" value="submit" /> <!-- 對應提交到submit事件上-->
        <input class="btn-submit" name="submit" accesskey="l" value="登陸" tabindex="4" type="submit" />
        <input class="btn-reset" name="reset" accesskey="c" value="重置" tabindex="5" type="reset" />
    </div>
</form>

當咱們點擊「登陸」後,首先就到 authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials), authenticationViaFormAction在cas-servlet.xml中配置:

<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
     p:centralAuthenticationService-ref="centralAuthenticationService"
     p:warnCookieGenerator-ref="warnCookieGenerator"/>

看doBind()方法:

public final void doBind(final RequestContext context, final Credentials credentials) throws Exception {
     final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
     // 在authenticationViaFormAction bean定義中並無注入credentialsBinder, 這裏也不會作什麼了
     if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) {
        this.credentialsBinder.bind(request, credentials);
     }
 }

接着看submit transition最終流向realSubmit:

<action-state id="realSubmit">
    <evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />   
    <transition on="warn" to="warn" />
    <transition on="success" to="sendTicketGrantingTicket" />
    <transition on="error" to="generateLoginTicket" />
    <transition on="accountDisabled" to="casAccountDisabledView" />
    <transition on="mustChangePassword" to="casMustChangePassView" />
    <transition on="accountLocked" to="casAccountLockedView" />
    <transition on="badHours" to="casBadHoursView" />
    <transition on="badWorkstation" to="casBadWorkstationView" />
    <transition on="passwordExpired" to="casExpiredPassView" />
</action-state>
看看authenticationViaFormAction的submit()方法:
public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext) throws Exception {
    // 首先驗證ticket的一致性
    final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);
    final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);
    if (!authoritativeLoginTicket.equals(providedLoginTicket)) {
        this.logger.warn("Invalid login ticket " + providedLoginTicket);
        final String code = "INVALID_TICKET";
        messageContext.addMessage(
             new MessageBuilder().error().code(code).arg(providedLoginTicket).defaultText(code).build());
        return "error";
    }
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
    final Service service = WebUtils.getService(context);
    if (StringUtils.hasText(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null) {
       try {
          final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service, credentials);
          WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
          putWarnCookieIfRequestParameterPresent(context);
          return "warn";
       } catch (final TicketException e) {
          if (isCauseAuthenticationException(e)) {
              populateErrorsInstance(e, messageContext);
              return getAuthenticationExceptionEventId(e);
          }
          this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
       }
    }
    try {
         WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials)); //這裏會調用AuthenticationManagerImpl的authenticateAndObtainPricipal方法,該方法會依次調用咱們在deployerConfigContext.xml中配置的authenticationManager bean的authenticationHandlers, 好比以前文章配置的數據庫認證處理器等,驗證成功了就會生成TGT(TicketGrantingTicket)返回給客戶端。
putWarnCookieIfRequestParameterPresent(context);
return "success";
} catch (final TicketException e) {
         populateErrorsInstance(e, messageContext);
         if (isCauseAuthenticationException(e))
                return getAuthenticationExceptionEventId(e);
         return "error";
    }
}

假如咱們登陸成功了,flow繼續流向sendTicketGrantingTicket state:

<action-state id="sendTicketGrantingTicket">
    <evaluate expression="sendTicketGrantingTicketAction" />
    <transition to="serviceCheck" />
</action-state>
看看SendTicketGrantingTicketAction作了什麼:

protected Event doExecute(final RequestContext context) {
        final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); 
        final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");
        
        if (ticketGrantingTicketId == null) {
            return success();
        }
        
        this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils.getHttpServletResponse(context), ticketGrantingTicketId);//將TGT做爲Cookie加到Response中

        if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
            this.centralAuthenticationService
                .destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
        }

        return success();
    }

返回後,繼續流向serviceCheck state, 會根據service是否爲空來決定怎麼流,也就是說,若是你是直接登陸/cas/login, 那麼就沒有service屬性,若是你是由其餘客戶端跳轉過來登陸的,那麼service就是那個客戶端跳轉登陸的url:

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

若是是直接登陸的cas服務器,登陸成功後,你就能夠看到下面的界面:

咱們假設是從你的另外一個web client跳轉過來的,那麼就會流向generateServiceTicket:

<action-state id="generateServiceTicket">
    <evaluate expression="generateServiceTicketAction" />
    <transition on="success" to ="warn" />
    <transition on="error" to="generateLoginTicket" />
    <transition on="gateway" to="gatewayServicesManagementCheck" />
</action-state>

看GenerateServiceTicketAction的doExecute方法:

protected Event doExecute(final RequestContext context) {
     final Service service = WebUtils.getService(context);
     final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);

     try {
          final String serviceTicketId = this.centralAuthenticationService
               .grantServiceTicket(ticketGrantingTicket,service); //根據TGT生成service ticket
          WebUtils.putServiceTicketInRequestScope(context, serviceTicketId); //放到request中
          return success();
     } catch (final TicketException e) {
            if (isGatewayPresent(context)) {
                return result("gateway");
            }
     }
     return error();
}
以後,又流向warn state, warnCookieValue就是咱們登陸界面上是否勾選了提示覆選框:

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

直接看redirect, 其主要構建Response對象,並放到requestScope中:

<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>
對於postRedirectDecision state,如果post過來的請求就到視圖就到 /WEB-INF/view/ jsp /protocol/casPostResponseView.jsp ,若get則外部跳轉到會以前的客戶端url

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

這就基本說了CAS服務整個登陸怎麼流動,下面也說說,咱們客戶端的處理流程。

-----------------------------------------------------------

web客戶端主要的配置就在web.xml中:

<listener>
    <listener-class>
        org.jasig.cas.client.session.SingleSignOutHttpSessionListener
    </listener-class>
    </listener>
    <filter>
        <filter-name>CasSingleSignOutFilter</filter-name>
	 <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CasSingleSignOutFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>CASFilter</filter-name>
        <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
        <init-param>
            <param-name>casServerLoginUrl</param-name>
            <param-value>https://localhost:8443/cas/login</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CASFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>CasTicketFilter</filter-name>
        <filter-class>
            org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
        <init-param>
            <param-name>casServerUrlPrefix</param-name>
            <param-value>https://localhost:8443/cas</param-value>
        </init-param>
        <init-param>
            <param-name>serverName</param-name>
            <param-value>http://localhost:8080</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CasTicketFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>CasRequestWrapFilter</filter-name>
        <filter-class>
            org.jasig.cas.client.util.HttpServletRequestWrapperFilter                                            </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CasRequestWrapFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>AssertionThreadLocalFilter</filter-name>
        <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>AssertionThreadLocalFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
SingleSignOutHttpSessionListener和SingleSignOutFilter用於登出操做。

CASFilter: 其doFilter方法實現:

public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        final HttpSession session = request.getSession(false);
        final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;

        if (assertion != null) { //有assertion信息(登陸信息)就經過
            filterChain.doFilter(request, response);
            return;
        }

        final String serviceUrl = constructServiceUrl(request, response);//獲取serviceUrl,即當前url
        final String ticket = CommonUtils.safeGetParameter(request,getArtifactParameterName());
        final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);

        if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { //若是有TGT就表示已登陸過了
            filterChain.doFilter(request, response);
            return;
        }
        final String modifiedServiceUrl;
        if (this.gateway) {
            log.debug("setting gateway attribute in session");
            modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
        } else {
            modifiedServiceUrl = serviceUrl;
        }
        final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); //即將要跳轉到CAS登陸界面的url及其一些參數
        response.sendRedirect(urlToRedirectTo);
    }
其中urlToRedirectTo相似:

https://${cas-server-host}:port/cas/login?service=http%3A%2F%2Flocalhost%3A8080%2Fcas-web-client1%2Findex.jsp

通過跳轉,而後登陸成功後的請求信息:

登陸成功之後咱們再訪問須要認證的url時,這時有了TGT, CAS服務端的login-webflow就有變化:

<decision-state id="ticketGrantingTicketExistsCheck">
    <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
</decision-state>
流向hasServiceCheck state:
<decision-state id="hasServiceCheck">
    <if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" />
</decision-state>
接着流向renewRequestCheck state:
<decision-state id="renewRequestCheck">
    <if test="requestParameters.renew != '' and requestParameters.renew != null" 
        then="serviceAuthorizationCheck" else="generateServiceTicket" />
</decision-state>
後面就和以前說的流程同樣了。

當咱們經過redirect返回以前的web客戶端時,還會發生什麼呢,這時有了TGT了,AuthenticationFilter中:

if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { //有TGT經過
     filterChain.doFilter(request, response);
     return;
}

因而接着web客戶端下一個的filter Cas20ProxyReceivingTicketValidationFilter:

<filter>
    <filter-name>CasTicketFilter</filter-name>
    <filter-class>
        org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
    <init-param>
        <param-name>casServerUrlPrefix</param-name>
        <param-value>https://localhost:8443/cas</param-value>
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://localhost:8080</param-value>
    </init-param>
</filter>

Cas20ProxyReceivingTicketValidationFilter過濾處理主要是其父類AbstractTicketValidationFilter實現:

public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
  //子類預處理,Cas20ProxyReceivingTicketValidationFliter作了一些處理
  if (!preFilter(servletRequest, servletResponse, filterChain)) {
     return;
  }

  final HttpServletRequest request = (HttpServletRequest) servletRequest;
  final HttpServletResponse response = (HttpServletResponse) servletResponse;
  final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());//獲取ticket

  if (CommonUtils.isNotBlank(ticket)) {
     try { 
      final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));//再次拿ticket到服務端驗證,看是否確實存在,或者是否過時, 默認實現爲Cas20ProxyTicketValidator
      request.setAttribute(CONST_CAS_ASSERTION, assertion);

      if (this.useSession) {//Aseesion放到session中,因此你就知道怎麼在咱們應用中訪問登陸的用戶信息了
          request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
      }
      onSuccessfulValidation(request, response, assertion);

      if (this.redirectAfterValidation) { // 默認true
          log. debug("Redirecting after successful ticket validation.");
          response.sendRedirect(constructServiceUrl(request, response));
          return;
      }
    }catch (final TicketValidationException e) {
          response.setStatus(HttpServletResponse.SC_FORBIDDEN);
          onFailedValidation(request, response);
          if (this.exceptionOnValidationFailure) {
              throw new ServletException(e);
          }
          return;
    }
  }
  filterChain.doFilter(request, response);
}

validate方法由AbstractBasedTicketValidator實現:

public Assertion validate(final String ticket, final String service) throws TicketValidationException {
      //獲取驗證url, 相似https:${cas-server-host}:port/cas/serviceValidate?ticket=xxx&service=yyy final String validationUrl = constructValidationUrl(ticket, service);
      if (log.isDebugEnabled()) {
            log.debug("Constructing validation url: " + validationUrl);
      }

      try {
            //發送請求並獲取返回內容(經過java URLConnection發送請求,直接讀取Response輸入流)
            final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);                                                                                           
            if (serverResponse == null) {
                throw new TicketValidationException("The CAS server returned no response.");
            }
            
            if (log.isDebugEnabled()) {
            	log.debug("Server response: " + serverResponse);
            }
            //解析CAS服務端返回的內容爲Assertion對象
            return parseResponseFromServer(serverResponse);
      } catch (final MalformedURLException e) {
            throw new TicketValidationException(e);
    }
}

上面發送認證請求後的返回內容相似:

<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationSuccess>
        <cas:user>admin</cas:user>
    </cas:authenticationSuccess>
</cas:serviceResponse>

驗證請求/cas/serviceValidate則對應服務器端配置的SafeDispatcherServlet:

這個Servlet中包含有一個咱們熟悉的Spring-MVC的前端分發器DispatcherServlet, 明顯由它來奮發咱們的請求,那麼/validateService對應那個Controller呢?看cas-servlet.xml配置:

看ServiceValidateController的handleRequestInternal方法重要的一句:

final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);

就是根據CentralAuthenticationServiceImpl的下面兩個變量來驗證:

/** TicketRegistry for storing and retrieving tickets as needed. */
@NotNull
private TicketRegistry ticketRegistry;
/** New Ticket Registry for storing and retrieving services tickets. Can point to the same one as the ticketRegistry variable. */
@NotNull
private TicketRegistry serviceTicketRegistry;

整個登陸基本流程簡單的瞭解over.

相關文章
相關標籤/搜索