jasig CAS登录验证分析

原创
2014/02/23 17:21
阅读数 1.2W

jasig CAS登录验证分析:

之前文章讲到了怎么利用jasig CAS实现sso:

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

本文对jasig CAS验证过程做个简单的分析,便于以后能够更好定制自己的CAS, 要了解CAS流程你需要知道spring,springmvc等知识,也要了解spring-webflow, 因为整个验证流程都是由spring-webflow定制的,你可以参考我转载的一篇spring-webflow的文章:

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

ok, 就开始了。

  • 先说说我们未登录状态时:

重点就是服务器端的配置: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中:

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

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

展开阅读全文
打赏
4
35 收藏
分享
加载中
真的非常详细!!!请教一下,我想把其他系统的接口注册到webflow中,可以这样操作吗?(在登录流程的中间,我需要调用一下另一个系统的接口)
2020/12/08 15:02
回复
举报
楼主您好,想向你请教一个问题:当我们已经登陆cas系统后,再次访问/login就会跳转到casGenericSuccess.jsp页面,请问此时我怎么获得我登陆的用户信息在此页面上显示,盼回复,谢谢啦。
2015/01/18 18:57
回复
举报
36 webflow是不是不能重复提交?!!
我把登录改成了ajax之后 只能登录成功一次,在点登录就是死循环!
但是登录失败就没问题!!
怎么破!!
2014/11/26 20:13
回复
举报
楼主写的太详细了 赞赞
2014/11/25 16:59
回复
举报
赞一个
2014/11/18 19:36
回复
举报
想请问 在AuthenticationViaFormAction 的
logger.warn("Invalid login ticket {}", providedLoginTicket);
messageContext.addMessage(new MessageBuilder().code("error.invalid.loginticket").build());
return newEvent(ERROR);
为什么 在 login page没有显现出来,是bug吗?
2014/10/30 14:27
回复
举报

引用来自“程序员jacky”的评论

在没登陆的时候,访问客服端会跳到登陆页面,比如:http://localhost:8088/login?service=http%3A%2F%2F。
然后登陆后会直接跳转到service后面的地址,如何登陆后跳到固定的页面,不管service后面是什么。

引用来自“inDestiny”的评论

那就把service配置为你要的那个固定的页面就行了,CAS会根据service去跳转,或者你就自己改CAS里的实现了。
试了,不行,因为改了service后面的url,每次都会进入固定的这个url,导致其他客户端没有收到票据
2014/10/30 10:09
回复
举报
ihaolin博主

引用来自“程序员jacky”的评论

在没登陆的时候,访问客服端会跳到登陆页面,比如:http://localhost:8088/login?service=http%3A%2F%2F。
然后登陆后会直接跳转到service后面的地址,如何登陆后跳到固定的页面,不管service后面是什么。
那就把service配置为你要的那个固定的页面就行了,CAS会根据service去跳转,或者你就自己改CAS里的实现了。
2014/10/29 21:04
回复
举报
在没登陆的时候,访问客服端会跳到登陆页面,比如:http://localhost:8088/login?service=http%3A%2F%2F。
然后登陆后会直接跳转到service后面的地址,如何登陆后跳到固定的页面,不管service后面是什么。
2014/10/29 16:50
回复
举报
大神,膜拜42
2014/10/23 15:41
回复
举报
更多评论
打赏
10 评论
35 收藏
4
分享
返回顶部
顶部