cas服务端登录流程源码分析

一.环境配置

1.源码版本

<version>4.1.11-SNAPSHOT</version>

2.服务端

http://www.eric.cas.server.com:8080/cas-server

3.cas client

http://www.eric.cas.client.com:8081/cas/index.do

4.cas-server-webapp的web.xml配置

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    <display-name>Central Authentication System (CAS) 4.1.0-SNAPSHOT</display-name>

    <context-param>
        <param-name>isLog4jAutoInitializationDisabled</param-name>
        <param-value>true</param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/spring-configuration/*.xml
            /WEB-INF/deployerConfigContext.xml
            <!-- this enables extensions and addons to contribute to overall CAS' application context
                 by loading spring context files from classpath i.e. found in classpath jars, etc. -->
            classpath*:/META-INF/spring/*.xml
        </param-value>
    </context-param>

    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>CAS Client Info Logging Filter</filter-name>
        <filter-class>org.jasig.inspektr.common.web.ClientInfoThreadLocalFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CAS Client Info Logging Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>requestParameterSecurityFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>requestParameterSecurityFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/status/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/statistics/*</url-pattern>
    </filter-mapping>

    <!--
      - Loads the CAS ApplicationContext.
    -->
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
    <listener>
        <listener-class>
            org.jasig.cas.CasEnvironmentContextListener
        </listener-class>
    </listener>
    <servlet>
        <servlet-name>cas</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/cas-servlet.xml, /WEB-INF/cas-servlet-*.xml</param-value>
        </init-param>
        <init-param>
            <param-name>publishContext</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet>
        <servlet-name>metrics-health</servlet-name>
        <servlet-class>com.codahale.metrics.servlets.HealthCheckServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>metrics</servlet-name>
        <servlet-class>com.codahale.metrics.servlets.MetricsServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>metrics-ping</servlet-name>
        <servlet-class>com.codahale.metrics.servlets.PingServlet</servlet-class>
    </servlet>

    <servlet>
        <servlet-name>metrics-threads</servlet-name>
        <servlet-class>com.codahale.metrics.servlets.ThreadDumpServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/login</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/logout</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/validate</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/serviceValidate</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/p3/serviceValidate</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/proxy</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/proxyValidate</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/p3/proxyValidate</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/CentralAuthenticationService</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/status</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/statistics</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/statistics/ssosessions</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/status/config</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>metrics-ping</servlet-name>
        <url-pattern>/statistics/ping</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>metrics</servlet-name>
        <url-pattern>/statistics/metrics</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>metrics-threads</servlet-name>
        <url-pattern>/statistics/threads</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>metrics-health</servlet-name>
        <url-pattern>/statistics/healthcheck</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/authorizationFailure.html</url-pattern>
    </servlet-mapping>
    <!-- REST support if cas-server-support-rest is included -->
    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/v1/*</url-pattern>
    </servlet-mapping>

    <!--
    <servlet-mapping>
        <servlet-name>cas</servlet-name>
        <url-pattern>/samlValidate</url-pattern>
    </servlet-mapping>
    -->

    <session-config>
        <!-- Default to 5 minute session timeouts -->
        <session-timeout>5</session-timeout>
        <tracking-mode>COOKIE</tracking-mode>
        <cookie-config>
            <http-only>true</http-only>
        </cookie-config>
    </session-config>

    <error-page>
        <error-code>401</error-code>
        <location>/authorizationFailure.html</location>
    </error-page>

    <error-page>
        <error-code>403</error-code>
        <location>/authorizationFailure.html</location>
    </error-page>

    <error-page>
        <error-code>404</error-code>
        <location>/</location>
    </error-page>

    <error-page>
        <error-code>500</error-code>
        <location>/WEB-INF/view/jsp/errors.jsp</location>
    </error-page>

    <error-page>
        <error-code>501</error-code>
        <location>/WEB-INF/view/jsp/errors.jsp</location>
    </error-page>

    <error-page>
        <error-code>503</error-code>
        <location>/WEB-INF/view/jsp/errors.jsp</location>
    </error-page>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>





二. 服务端登录流程解析

首先找到服务端登录流程配置文件login-webflow.xml

/WEB-INF/webflow/login/login-webflow.xml

1. 访问Cas client 地址

http://www.eric.cas.client.com:8081/cas/index.do

2. 第一次访问,用户未登录,则跳转到cas server进行登录认证

http://www.eric.cas.server.com:8080/cas-server/login?service=http://www.eric.cas.client.com:8081/cas/index.do

请求到达服务端,登录流程具体怎么开始呢?
具体登录流程信息,可以查看login-webflow.xml配置文件

login-webflow.xml部分配置代码
#设置变量存储登录信息
<var name="credential" class="org.jasig.cas.authentication.RememberMeUsernamePasswordCredential"/>
// 登录流程开始 初始化
<on-start>
    <evaluate expression="initialFlowSetupAction"/>
</on-start>
(1). 设置变量
(2). 初始化initialFlowSetupAction 具体初始化信息配置在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"
        p:servicesManager-ref="servicesManager"
        p:enableFlowOnAbsentServiceRequest="${create.sso.missing.service:true}"  />

以下参数在配置文件WEB-INF/spring-configuration对应的配置文件中

  • argumentExtractors,
  • warnCookieGenerator
  • ticketGrantingTicketCookieGenerator

argumentExtractorsConfiguration.xml
cas参数提取配置 主要用于提取cas client发送过来的请求参数

    <bean
            id="casArgumentExtractor"
            class="org.jasig.cas.web.support.CasArgumentExtractor"/>

    <util:list id="argumentExtractors">
        <!-- <ref bean="samlArgumentExtractor" /> -->
        <ref bean="casArgumentExtractor"/>
    </util:list>

warnCookieGenerator.xml

#设置warnCookie信息
<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
      p:cookieHttpOnly="true"
      p:cookieSecure="false"
      p:cookieMaxAge="-1"
      p:cookieName="CASPRIVACY"
      p:cookiePath=""/>

ticketGrantingTicketCookieGenerator.xml
TGT生成策略配置

   <bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
          c:casCookieValueManager-ref="cookieValueManager"
          p:cookieSecure="false"
          p:cookieMaxAge="-1"
          p:cookieName="TGC"
          p:rememberMeMaxAge="7200"
          p:cookiePath=""/>

    <bean id="cookieCipherExecutor" class="org.jasig.cas.util.BaseStringCipherExecutor"
          c:secretKeyEncryption="${tgc.encryption.key:}"
          c:secretKeySigning="${tgc.signing.key:}"/>

    <bean id="cookieValueManager" class="org.jasig.cas.web.support.DefaultCasCookieValueManager"
          c:cipherExecutor-ref="cookieCipherExecutor"/>

初始化部分会调用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作用域和RequestScope中
        
        WebUtils.putTicketGrantingTicketInScopes(context,
                this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
        //将warnCookieValue放在FlowScope作用域中
        
        WebUtils.putWarningCookie(context,
                Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
                
        //根据配置的参数提取器argumentExtractors获取service参数
        
        final Service service = WebUtils.getService(this.argumentExtractors, context);


        if (service != null) {
            logger.debug("Placing service in context scope: [{}]", service.getId());
            
            //根据请求service匹配cas-server-webapp/src/main/resources/services下定义的service
            
            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");
    }

Service 属性图


image

RegisteredService 属性图


image

InitialFlowSetupAction的doExecute要做的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域中,以便在登录流程中的state中进行判断。初始化完成后,登录流程流转到第一个state(ticketGrantingTicketCheck)。

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

    protected Event doExecute(final RequestContext requestContext) throws Exception {
        // 查询tgtId
        final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
        if (!StringUtils.hasText(tgtId)) {
            return new Event(this, NOT_EXISTS);
        }

        String eventId = INVALID;
        try {
            final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
            if (ticket != null && !ticket.isExpired()) {
                eventId = VALID;
            }
        } catch (final TicketException e) {
            logger.trace("Could not retrieve ticket id {} from registry.", e);
        }
        return new Event(this,  eventId);
    }

由于第一次访问没有登录,tgtId为null,则跳转到第二个state

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

    <!-- Do a service authorization check early without the need to login first -->
    <action-state id="serviceAuthorizationCheck">
        <evaluate expression="serviceAuthorizationCheck"/>
        <transition to="generateLoginTicket"/>
    </action-state>

执行ServiceAuthorizationCheck的doExecute方法

 @Override
    protected Event doExecute(final RequestContext context) throws Exception {
        final Service service = WebUtils.getService(context);
        //No service == plain /login request. Return success indicating transition to the login form
        if (service == null) {
            return success();
        }
        
        if (this.servicesManager.getAllServices().isEmpty()) {
            final String msg = String.format("No service definitions are found in the service manager. "
                    + "Service [%s] will not be automatically authorized to request authentication.", service.getId());
            logger.warn(msg);
            throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_EMPTY_SVC_MGMR);
        }
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

        if (registeredService == null) {
            final String msg = String.format("Service Management: Unauthorized Service Access. "
                    + "Service [%s] is not found in service registry.", service.getId());
            logger.warn(msg);
            throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
        }
        if (!registeredService.getAccessStrategy().isServiceAccessAllowed()) {
            final String msg = String.format("Service Management: Unauthorized Service Access. "
                    + "Service [%s] is not allowed access via the service registry.", service.getId());
            
            logger.warn(msg);

            WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context,
                    registeredService.getAccessStrategy().getUnauthorizedRedirectUrl());
            throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
        }

        return success();
    }

ServiceAuthorizationCheck的doExecute方法,要做的就是判断FlowScope作用域中是否存在service,如果service存在,查找service的注册信息。登录流程流转到第四个state(generateLoginTicket)。

    <action-state id="generateLoginTicket">
        <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)"/>
        <transition on="generated" to="viewLoginForm"/>
    </action-state>

执行GenerateLoginTicketAction的generate方法

    public final String generate(final RequestContext context) {
        final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);
        logger.debug("Generated login ticket {}", loginTicket);
        // 保存loginTicket到FlowScope作用域中
        WebUtils.putLoginTicket(context, loginTicket);
        return "generated";
    }

GenerateLoginTicketAction的generate要做的就是生成loginTicket,并且把loginTicket放到FlowScope作用域中。登录流程流转到第五个state(viewLoginForm)。

/WEB-INF/cas-servlet.xml部分代码

<bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction"  
    p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator" />  

/WEB-INF/spring-configuration/uniqueIdGenerators.xml部分代码

<bean id="loginTicketUniqueIdGenerator" class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator">  
    <constructor-arg  
        index="0"  
        type="int"  
        value="30" />  
</bean>

DefaultUniqueTicketIdGenerator要做的就是生成以LT作为前缀的loginTicket(例:LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn)。

==注:LT只作为登录时使用的票据。==

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

            <!--
            <evaluate expression="samlMetadataUIParserAction" />
            -->
        </on-entry>
        <transition on="submit" bind="true" validate="true" to="realSubmit"/>
    </view-state>

至此,经过五个state的流转,我们完成了第一次访问集成了单点登录的应用系统,此时流转到CAS单点登录服务器端的登录页面/WEB-INF/jsp/ui/default/casLoginView.jsp。

由于casLoginView.jsp是CAS提供的默认登录页面,需要把此页面修改成我们系统需要的登录页面,格式需要参考casLoginView.jsp。

注意:
默认的登录页面中有lt、execution和_eventId三个隐藏参数,lt参数值就是在GenerateLoginTicketAction的generate方法中生成的loginTicket。

<input type="hidden" name="lt" value="${loginTicket}" />
<input type="hidden" name="execution" value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />

登录页面输入用户名和密码,登录cas

    <action-state id="realSubmit">
        <evaluate
                expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)"/>
        <transition on="warn" to="warn"/>
        <!--
        To enable AUP workflows, replace the 'success' transition with the following:
        <transition on="success" to="acceptableUsagePolicyCheck" />
        -->
        <transition on="success" to="sendTicketGrantingTicket"/>
        <transition on="successWithWarnings" to="showMessages"/>
        <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
        <transition on="error" to="generateLoginTicket"/>
    </action-state>

执行authenticationViaFormAction的submit方法

    public final Event submit(final RequestContext context, final Credential credential,
                              final MessageContext messageContext) {
        //验证请求参数中loginTicket和FlowScope作用域中loginTicket是否存在且相等
        if (!checkLoginTicketIfExists(context)) {
            return returnInvalidLoginTicketEvent(context, messageContext);
        }
        
        // 判断是否已经存在TGT
        if (isRequestAskingForServiceTicket(context)) {
            return grantServiceTicket(context, credential);
        }
        //根据用户凭证构造TGT,把TGT放到requestScope中,同时把TGT缓存到服务器的cache<ticketId,TGT>中  
        return createTicketGrantingTicket(context, credential, messageContext);
    }

由于是第一次登陆TGT为空,执行createTicketGrantingTicket方法如下:

/**
     * Create ticket granting ticket for the given credentials.
     * Adds all warnings into the message context.
     *
     * @param context the context
     * @param credential the credential
     * @param messageContext the message context
     * @return the resulting event.
     * @since 4.1.0
     */
    protected Event createTicketGrantingTicket(final RequestContext context, final Credential credential,
                                               final MessageContext messageContext) {
        try {
            // 根据认证信息创建TGT 此时并没有把TGT和对应的service做关联
            final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(credential);
            WebUtils.putTicketGrantingTicketInScopes(context, tgt);
            putWarnCookieIfRequestParameterPresent(context);
            putPublicWorkstationToFlowIfRequestParameterPresent(context);
            if (addWarningMessagesToMessageContextIfNeeded(tgt, messageContext)) {
                return newEvent(SUCCESS_WITH_WARNINGS);
            }
            return newEvent(SUCCESS);
        } catch (final AuthenticationException e) {
            logger.debug(e.getMessage(), e);
            return newEvent(AUTHENTICATION_FAILURE, e);
        } catch (final Exception e) {
            logger.debug(e.getMessage(), e);
            return newEvent(ERROR, e);
        }
    }

创建TGT实际调用的是CentralAuthenticationServiceImpl类的createTicketGrantingTicket方法 代码如下

    public TicketGrantingTicket createTicketGrantingTicket(final Credential... credentials)
            throws AuthenticationException, TicketException {
        //认证失败会直接返回错误提示信息 流程终止
        final Set<Credential> sanitizedCredentials = sanitizeCredentials(credentials);
        if (!sanitizedCredentials.isEmpty()) {
            final Authentication authentication = this.authenticationManager.authenticate(credentials);
            // 根据ticketExpirationPolicies.xml配置生成TGT对象
            final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
                    this.ticketGrantingTicketUniqueTicketIdGenerator
                            .getNewTicketId(TicketGrantingTicket.PREFIX),
                    authentication, this.ticketGrantingTicketExpirationPolicy);
            //根据ticketRegistry.xml配置保存TGT到缓存中
            this.ticketRegistry.addTicket(ticketGrantingTicket);
            return ticketGrantingTicket;
        }
        final String msg = "No credentials were specified in the request for creating a new ticket-granting ticket";
        logger.warn(msg);
        throw new TicketCreationException(new IllegalArgumentException(msg));
    }

注意:
可以按照需要配置ticketRegistry.xml,把TGT缓存到Redis等内存数据库中

既然是登录,那么可以在此方法中加入自己的业务逻辑,比如,可以加入验证码的判断,以及错误信息的提示,用户名或者密码错误,验证码错误等逻辑判断。

生成TGT成功 返回success 跳转到流程sendTicketGrantingTicket

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

执行SendTicketGrantingTicketAction的doExecute方法

protected Event doExecute(final RequestContext context) {
       //requestScope和FlowScope中获取TGT 
        final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
        final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");

        if (ticketGrantingTicketId == null) {
            return success();
        }

        if (isAuthenticatingAtPublicWorkstation(context)) {
            LOGGER.info("Authentication is at a public workstation. "
                    + "SSO cookie will not be generated. Subsequent requests will be challenged for authentication.");
        } else if (!this.createSsoSessionCookieOnRenewAuthentications && isAuthenticationRenewed(context)) {
            LOGGER.info("Authentication session is renewed but CAS is not configured to create the SSO session. "
                    + "SSO cookie will not be generated. Subsequent requests will be challenged for authentication.");
        } else {
        
             //response中添加TGC的cookie
             
            this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils
                    .getHttpServletResponse(context), ticketGrantingTicketId);
        }

        if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
            LOGGER.debug("Found Ticket-granting ticket mismatch. Removing [{}]", ticketGrantingTicketValueFromCookie);
            this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
        }

        return success();
    }

SendTicketGrantingTicketAction的doExecute要做的是获取TGT,并根据TGT生成cookie添加到response。登录流程流转到第三个state(serviceCheck)。

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

由于此时FlowScope中存在service=http://www.eric.cas.client.com:8081/cas/index.do,登录流程流转到第四个state(generateServiceTicket)

    <action-state id="generateServiceTicket">
        <evaluate expression="generateServiceTicketAction"/>
        <transition on="success" to="warn"/>
        <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
        <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);

        logger.debug("Attempting to generate service ticket based on [{}] and for [{}]", ticketGrantingTicket, service);
        try {
            // 根据TGT和service生成ST 并将service保存到TGT的services对象中  建立关联关系
            
            final ServiceTicket serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket, service);
            logger.debug("Created service ticket [{}]", serviceTicketId);
            
            //ST放到requestScope中
            
            WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
            return success();
        } catch (final TicketException e) {
            logger.error(e.getMessage(), e);

            if (e instanceof InvalidTicketException) {
                logger.debug("Ticket is deemed invalid. Destroying [{}]", ticketGrantingTicket);
                this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
            }
            if (isGatewayPresent(context)) {
                return result("gateway");
            }

            return newEvent("error", e);
        }
    }

GenerateServiceTicketAction的doExecute方法要做的是根据service和TGT生成以ST为前缀的serviceTicket(例:ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org),并将service保存到TGT的services对象中 ,建立关联关系,同时把serviceTicket放到requestScope中。

CentralAuthenticationServiceImpl的grantServiceTicket方法

@Audit(
            action = "SERVICE_TICKET",
            actionResolverName = "GRANT_SERVICE_TICKET_RESOLVER",
            resourceResolverName = "GRANT_SERVICE_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "GRANT_SERVICE_TICKET_TIMER")
    @Metered(name = "GRANT_SERVICE_TICKET_METER")
    @Counted(name = "GRANT_SERVICE_TICKET_COUNTER", monotonic = true)
    @Override
    public ServiceTicket grantServiceTicket(
            final String ticketGrantingTicketId,
            final Service service, final Credential... credentials)
            throws AuthenticationException, TicketException {

        logger.debug("Attempting to get ticket id {} to create service ticket", ticketGrantingTicketId);
        final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

        verifyRegisteredServiceProperties(registeredService, service);
        final Set<Credential> sanitizedCredentials = sanitizeCredentials(credentials);

        if (sanitizedCredentials.isEmpty() && !registeredService.getAccessStrategy().isServiceAccessAllowedForSso()) {
            if (ticketGrantingTicket.getProxiedBy() != null) {
                logger.warn("ServiceManagement: Service [{}] is not allowed to use SSO for proxying.", service.getId());
                throw new UnauthorizedSsoServiceException();
            } else if (ticketGrantingTicket.getProxiedBy() == null && ticketGrantingTicket.getCountOfUses() > 0) {
                logger.warn("ServiceManagement: Service [{}] is not allowed to use SSO.", service.getId());
                throw new UnauthorizedSsoServiceException();
            }
        }

        Authentication currentAuthentication = null;
        if (!sanitizedCredentials.isEmpty()) {
            logger.debug("Attempting to authenticate sanitized credentials to create service ticket");
            currentAuthentication = this.authenticationManager.authenticate(
                    sanitizedCredentials.toArray(new Credential[]{}));
            final Authentication original = ticketGrantingTicket.getAuthentication();
            if (!currentAuthentication.getPrincipal().equals(original.getPrincipal())) {
                logger.debug("Principal associated with current authentication {} does not match "
                                + "the principal {} associated with the original authentication",
                        currentAuthentication.getPrincipal(), original.getPrincipal());
                throw new MixedPrincipalException(
                        currentAuthentication, currentAuthentication.getPrincipal(), original.getPrincipal());
            }
            logger.debug("Added authentication to the collection of supplemental authentications");
            ticketGrantingTicket.getSupplementalAuthentications().add(currentAuthentication);
        }

        final Service proxiedBy = ticketGrantingTicket.getProxiedBy();
        if (proxiedBy != null) {
            logger.debug("TGT is proxied by [{}]. Locating proxy service in registry...", proxiedBy.getId());
            final RegisteredService proxyingService = servicesManager.findServiceBy(proxiedBy);

            if (proxyingService != null) {
                logger.debug("Located proxying service [{}] in the service registry", proxyingService);
                if (!proxyingService.getProxyPolicy().isAllowedToProxy()) {
                    logger.warn("Found proxying service {}, but it is not authorized to fulfill the proxy attempt made by {}",
                            proxyingService.getId(), service.getId());
                    throw new UnauthorizedProxyingException("Proxying is not allowed for registered service "
                            + registeredService.getId());
                }
            } else {
                logger.warn("No proxying service found. Proxy attempt by service [{}] (registered service [{}]) is not allowed.",
                        service.getId(), registeredService.getId());
                throw new UnauthorizedProxyingException("Proxying is not allowed for registered service "
                        + registeredService.getId());
            }
        } else {
            logger.debug("TGT is not proxied by another service");
        }

        // Perform security policy check by getting the authentication that satisfies the configured policy
        // This throws if no suitable policy is found
        logger.debug("Checking for authentication policy satisfaction...");
        getAuthenticationSatisfiedByPolicy(ticketGrantingTicket.getRoot(), new ServiceContext(service, registeredService));

        final List<Authentication> authentications = ticketGrantingTicket.getChainedAuthentications();
        final Principal principal = authentications.get(authentications.size() - 1).getPrincipal();

        logger.debug("Located principal {} for service ticket creation", principal);

        final Map<String, Object> principalAttrs = registeredService.getAttributeReleasePolicy().getAttributes(principal);
        if (!registeredService.getAccessStrategy().doPrincipalAttributesAllowServiceAccess(principalAttrs)) {
            logger.warn("ServiceManagement: Cannot grant service ticket because Service [{}] is not authorized for use by [{}].",
                    service.getId(), principal);
            throw new UnauthorizedServiceForPrincipalException();
        }

        final String uniqueTicketIdGenKey = service.getClass().getName();
        logger.debug("Looking up service ticket id generator for [{}]", uniqueTicketIdGenKey);
        UniqueTicketIdGenerator serviceTicketUniqueTicketIdGenerator =
                this.uniqueTicketIdGeneratorsForService.get(uniqueTicketIdGenKey);
        if (serviceTicketUniqueTicketIdGenerator == null) {
            serviceTicketUniqueTicketIdGenerator = this.defaultServiceTicketIdGenerator;
            logger.debug("Service ticket id generator not found for [{}]. Using the default generator...",
                    uniqueTicketIdGenKey);
        }

        final String ticketPrefix = authentications.size() == 1 ? ServiceTicket.PREFIX : ServiceTicket.PROXY_TICKET_PREFIX;
        final String ticketId = serviceTicketUniqueTicketIdGenerator.getNewTicketId(ticketPrefix);
        logger.debug("Created new ticket id {}", ticketId);
        // 生成ST 实际调用TicketGrantingTicketImpl的grantServiceTicket方法
        final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket(
                ticketId,
                service,
                this.serviceTicketExpirationPolicy,
                currentAuthentication != null);

        logger.info("Granted ticket [{}] for service [{}] for user [{}]",
                serviceTicket.getId(), service.getId(), principal.getId());
        this.ticketRegistry.addTicket(serviceTicket);
        logger.debug("Added service ticket {} to ticket registry", serviceTicket.getId());

        return serviceTicket;
    }

TicketGrantingTicketImpl的grantServiceTicket方法

/**
     * {@inheritDoc}
     * <p>The state of the ticket is affected by this operation and the
     * ticket will be considered used. The state update subsequently may
     * impact the ticket expiration policy in that, depending on the policy
     * configuration, the ticket may be considered expired.
     */
    @Override
    public synchronized ServiceTicket grantServiceTicket(final String id,
        final Service service, final ExpirationPolicy expirationPolicy,
        final boolean credentialsProvided) {
        final ServiceTicket serviceTicket = new ServiceTicketImpl(id, this,
            service, this.getCountOfUses() == 0 || credentialsProvided,
            expirationPolicy);

        updateState();

        final List<Authentication> authentications = getChainedAuthentications();
        service.setPrincipal(authentications.get(authentications.size()-1).getPrincipal());
        // 按照key为ST,value为service的方式保存信息到TGT的services中
        this.services.put(id, service);

        return serviceTicket;
    }

TicketGrantingTicketImpl的grantServiceTicket方法负责生成ST 并保存service和ST到services中

登录流程流转到第五个state(warn)

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

由于此时FlowScope中不存在warnCookieValue,登录流程流转到第六个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>

从requestScope中获取serviceTicket,构造response对象,并把response放到requestScope中。登录流程流转到第七个state(postRedirectDecision)

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

由于request请求(http://www.eric.cas.server.com:8080/login?service=http://www.eric.cas.client.com:8081/cas/index.do)是get类型,

登录流程流转到第八个state(redirectView)

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

此时流程如下:

知识点:
CentralAuthenticationServiceImpl类是CentralAuthenticationService的实现类,有关TGT的生成,保存,删除,验证都在
该类中实现。

至此CAS第一次登陆的完整流程结束。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,252评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,886评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,814评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,869评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,888评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,475评论 1 312
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,010评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,924评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,469评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,552评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,680评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,362评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,037评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,519评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,621评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,099评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,691评论 2 361

推荐阅读更多精彩内容