CAS流程简析 服务端处理未携带Service登录请求

相关阅读

login请求的处理流程配置

/WEB-INF/web.xml中欢迎页的配置如下:

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

index.jsp的内容如下:

<%@ page language="java"  session="false" %>

<%
final String queryString = request.getQueryString();
final String url = request.getContextPath() + "/login" + (queryString != null ? '?' + queryString : "");
response.sendRedirect(response.encodeURL(url));%>

将访问请求重定向至http://ip:port/cas/login?queryString
/WEB-INF/web.xml中定义了拦截处理login请求的servlet,配置如下:

<servlet>
    <servlet-name>cas</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <!-- Load the child application context. Start with the default, then modules, then overlays. -->
        <param-value>/WEB-INF/cas-servlet.xml,classpath*:/META-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>
    <async-supported>true</async-supported>
</servlet>

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

/WEB-INF/web.xml会引入/WEB-INF/cas-servlet.xml,该文件定义了login请求的映射逻辑,相关配置如下:

<!-- cas-servlet.xml -->
<!-- login webflow configuration -->
<bean id="loginFlowHandlerMapping" class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping"
      p:flowRegistry-ref="loginFlowRegistry" p:order="2">
    <property name="interceptors">
        <array value-type="org.springframework.web.servlet.HandlerInterceptor">
            <ref bean="localeChangeInterceptor"/>
            <ref bean="authenticationThrottle"/>
        </array>
    </property>
</bean>

<bean name="loginFlowExecutor" class="org.springframework.webflow.executor.FlowExecutorImpl"
      c:definitionLocator-ref="loginFlowRegistry"
      c:executionFactory-ref="loginFlowExecutionFactory"
      c:executionRepository-ref="loginFlowExecutionRepository"/>

<bean name="loginFlowExecutionFactory" class="org.springframework.webflow.engine.impl.FlowExecutionImplFactory"
      p:executionKeyFactory-ref="loginFlowExecutionRepository"/>

<bean id="loginFlowExecutionRepository" class=" org.jasig.spring.webflow.plugin.ClientFlowExecutionRepository"
      c:flowExecutionFactory-ref="loginFlowExecutionFactory"
      c:flowDefinitionLocator-ref="loginFlowRegistry"
      c:transcoder-ref="loginFlowStateTranscoder"/>

<bean id="loginFlowStateTranscoder" class="org.jasig.spring.webflow.plugin.EncryptedTranscoder"
      c:cipherBean-ref="loginFlowCipherBean" />

loginFlowRegistry配置在/WEB-INF/spring-configuration/webflowContext.xml中,该配置文件由/WEB-INF/web.xml文件引入,具体如下:

<!-- web.xml -->
<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>

配置文件/WEB-INF/spring-configuration/webflowContext.xmlloginFlowRegistry的配置如下:

<!-- webflowContext.xml -->
<webflow:flow-registry id="loginFlowRegistry" flow-builder-services="builder" base-path="/WEB-INF/webflow">
    <webflow:flow-location-pattern value="/login/*-webflow.xml"/>
</webflow:flow-registry>

定义了流程配置文件路径:/WEB-INF/webflow/login/*-webflow.xml,即/WEB-INF/webflow/login/login-webflow.xml,该文件定义了login请求的处理流程;

login请求的处理流程开启

根据/WEB-INF/cas-servlet.xml的配置:

<bean id="loginHandlerAdapter" class="org.jasig.cas.web.flow.SelectiveFlowHandlerAdapter"
      p:supportedFlowId="login" p:flowExecutor-ref="loginFlowExecutor" p:flowUrlHandler-ref="loginFlowUrlHandler"/>

...
<bean name="loginFlowExecutor" class="org.springframework.webflow.executor.FlowExecutorImpl"
      c:definitionLocator-ref="loginFlowRegistry"
      c:executionFactory-ref="loginFlowExecutionFactory"
      c:executionRepository-ref="loginFlowExecutionRepository"/>

p:supportedFlowId="login"可知,请求http://ip:port/cas/login?queryString会被loginHandlerAdapter处理,其定义的loginFlowExecutor属性会引用到loginFlowRegistry,即/WEB-INF/webflow/login/login-webflow.xml文件中配置的处理流程;
先分析SelectiveFlowHandlerAdapter的处理,其核心逻辑继承自父类FlowHandlerAdapter,代码如下:

public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    FlowHandler flowHandler = (FlowHandler)handler;
    this.checkAndPrepare(request, response, false);
    // 根据cas-servlet.xml配置<bean id="loginFlowUrlHandler" class="org.jasig.cas.web.flow.CasDefaultFlowUrlHandler"/>
    // flowUrlHandler为CasDefaultFlowUrlHandler
    // getFlowExecutionKey(request)的实现为:request.getParameter("execution")
    String flowExecutionKey = this.flowUrlHandler.getFlowExecutionKey(request);
    // 第一次请求,request的"execution"属性为null
    if (flowExecutionKey != null) {
        try {
            ServletExternalContext context = this.createServletExternalContext(request, response);
            FlowExecutionResult result = this.flowExecutor.resumeExecution(flowExecutionKey, context);
            this.handleFlowExecutionResult(result, context, request, response, flowHandler);
        } catch (FlowException var11) {
            this.handleFlowException(var11, request, response, flowHandler);
        }
    } else {
        try {
            // flowId即"login"
            String flowId = this.getFlowId(flowHandler, request);
            MutableAttributeMap<Object> input = this.getInputMap(flowHandler, request);
            ServletExternalContext context = this.createServletExternalContext(request, response);
            // 启动登录流程处理
            FlowExecutionResult result = this.flowExecutor.launchExecution(flowId, input, context);
            this.handleFlowExecutionResult(result, context, request, response, flowHandler);
        } catch (FlowException var10) {
            this.handleFlowException(var10, request, response, flowHandler);
        }
    }

    return null;
}

其中this.flowExecutor.launchExecutionFlowExecutorImpl.launchExecution,代码如下:

public FlowExecutionResult launchExecution(String flowId, MutableAttributeMap<?> input, ExternalContext context) throws FlowException {
    FlowExecutionResult var6;
    try {
        if (logger.isDebugEnabled()) {
            logger.debug("Launching new execution of flow '" + flowId + "' with input " + input);
        }

        ExternalContextHolder.setExternalContext(context);
        // 根据loginFlowRegistry加载流程定义
        FlowDefinition flowDefinition = this.definitionLocator.getFlowDefinition(flowId);
        // 根据loginFlowExecutionFactory创建流程
        FlowExecution flowExecution = this.executionFactory.createFlowExecution(flowDefinition);
        // 开启流程
        flowExecution.start(input, context);
        if (!flowExecution.hasEnded()) {
            FlowExecutionLock lock = this.executionRepository.getLock(flowExecution.getKey());
            lock.lock();

            try {
                this.executionRepository.putFlowExecution(flowExecution);
            } finally {
                lock.unlock();
            }

            FlowExecutionResult var7 = this.createPausedResult(flowExecution);
            return var7;
        }

        var6 = this.createEndResult(flowExecution);
    } finally {
        ExternalContextHolder.setExternalContext((ExternalContext)null);
    }

    return var6;
}

login请求的处理流程简析

1 on-start

login请求的处理流程定义在/WEB-INF/webflow/login/login-webflow.xml文件中;
先是执行<on-start>动作,配置如下:

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

initialFlowSetupAction对应的是InitialFlowSetupAction,其父类AbstractAction定义了执行的算法模板,代码如下:

public final Event execute(RequestContext context) throws Exception {
    Event result = this.doPreExecute(context);
    if (result == null) {
        // 启动处理流程
        result = this.doExecute(context);
        this.doPostExecute(context);
    } else if (this.logger.isInfoEnabled()) {
        this.logger.info("Action execution disallowed; pre-execution result is '" + result.getId() + "'");
    }

    return result;
}

InitialFlowSetupAction实现了算法细节doExecute,代码如下:

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 + '/' : "/";

    // 设置cookie路径
    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());
    }

    // 保存cookie中的TGT信息
    WebUtils.putTicketGrantingTicketInScopes(context,
            this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));

    // 保存cookie中的WARNING信息
    WebUtils.putWarningCookie(context,
            Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));

    // 获取请求中携带的Service信息
    final Service service = WebUtils.getService(this.argumentExtractors, context);

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

        // 根据请求携带的Service信息查找已注册的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());
            // 保存已注册Service信息
            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());
                // 保存重定向URL信息
                WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl());
            }
        }
    } else if (!this.enableFlowOnAbsentServiceRequest) {
        // 不支持空Service请求
        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信息
    WebUtils.putService(context, service);
    return result("success");
}

2 action-state

因为没有start-state,则根据流程配置执行第一个action-state,配置如下:

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

ticketGrantingTicketCheck对应的是TicketGrantingTicketCheckAction,继承自AbstractAction,实现了算法细节doExecute,代码如下:

protected Event doExecute(final RequestContext requestContext) throws Exception {
    // 从上下文中获取TGT信息
    final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
    if (!StringUtils.hasText(tgtId)) {
        // 不存在,则返回NOT_EXISTS事件
        return new Event(this, NOT_EXISTS);
    }

    // 默认TGT无效
    String eventId = INVALID;
    try {
        // 获取ticket信息
        final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
        if (ticket != null && !ticket.isExpired()) {
            // TGT有效
            eventId = VALID;
        }
    } catch (final AbstractTicketException e) {
        logger.trace("Could not retrieve ticket id {} from registry.", e);
    }
    return new Event(this,  eventId);
}

login请求时,cookie中不存在TGT信息,所以走notExists处理,即gatewayRequestCheck,其配置如下:

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

根据表达式可知,未携带Service信息的login请求,会走serviceAuthorizationCheck处理,其配置如下:

<!-- login-webflow.xml -->
<action-state id="serviceAuthorizationCheck">
    <evaluate expression="serviceAuthorizationCheck"/>
    <transition to="initializeLogin"/>
</action-state>

serviceAuthorizationCheck对应的是ServiceAuthorizationCheck,继承自AbstractAction,实现了算法细节doExecute,代码如下:

protected Event doExecute(final RequestContext context) throws Exception {
    // 从上下文中获取Service信息
    final Service service = WebUtils.getService(context);
    //No service == plain /login request. Return success indicating transition to the login form
    if (service == null) {
        // 不存在Service直接返回
        return success();
    }
    
    if (this.servicesManager.getAllServices().isEmpty()) {
        // 未配置任何Service
        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);
    }
    // 获取匹配的已注册Service
    final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

    if (registeredService == null) {
        // 无匹配的已注册Service,则授权校验失败
        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);
        // 设置重定向URL
        WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context,
                registeredService.getAccessStrategy().getUnauthorizedRedirectUrl());
        throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
    }

    return success();
}

login请求未携带Service信息,所以走initializeLogin处理,其配置如下:

<!-- login-webflow.xml -->
<action-state id="initializeLogin">
    <evaluate expression="'success'"/>
    <transition on="success" to="viewLoginForm"/>
</action-state>

根据表达式的值,直接走viewLoginForm处理,其配置如下:

<!-- login-webflow.xml -->
<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>
<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>

根据urlBasedViewResolver的配置:

<!-- cas-servlet.xml -->
<bean id="urlBasedViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"
      p:viewClass="org.springframework.web.servlet.view.InternalResourceView"
      p:prefix="${cas.themeResolver.pathprefix:/WEB-INF/view/jsp}/"
      p:suffix=".jsp"
      p:order="2000"/>

以及RegisteredServiceThemeBasedViewResolver.THEME_LOCATION_PATTERN(即%s/%s/ui/)参数值,通过代码

// viewName为casLoginView
final String defaultThemePrefix = String.format(THEME_LOCATION_PATTERN, getPrefix(), "default");
final String defaultViewUrl = defaultThemePrefix + viewName + getSuffix();

可知,casLoginView对应的页面为/WEB-INF/view/jsp/default/ui/casLoginView.jsp,该页面主要内容如下:

<form:input cssClass="required" cssErrorClass="error" id="username" size="25" tabindex="1" accesskey="${userNameAccessKey}" path="username" autocomplete="off" htmlEscape="true" />
<form:password cssClass="required" cssErrorClass="error" id="password" size="25" tabindex="2" path="password"  accesskey="${passwordAccessKey}" htmlEscape="true" autocomplete="off" />
<input type="hidden" name="execution" value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
<input class="btn-submit" name="submit" accesskey="l" value="<spring:message code="screen.welcome.button.login" />" tabindex="6" type="submit" />
<input class="btn-reset" name="reset" accesskey="c" value="<spring:message code="screen.welcome.button.clear" />" tabindex="7" type="reset" />

casLoginView.jsp页面提交的参数绑定到UsernamePasswordCredential对象中,页面提交后,走realSubmit处理,其配置如下:

<!-- login-webflow.xml -->
<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="initializeLogin"/>
</action-state>

authenticationViaFormAction对应的是AuthenticationViaFormAction,其submit方法的代码如下:

public final Event submit(final RequestContext context, final Credential credential,
                          final MessageContext messageContext)  {
    if (isRequestAskingForServiceTicket(context)) {
        // 若请求ST,则创建ST
        return grantServiceTicket(context, credential);
    }

    // 创建TGT
    return createTicketGrantingTicket(context, credential, messageContext);
}

protected boolean isRequestAskingForServiceTicket(final RequestContext context) {
    // 从上下文中获取TGT信息
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
    // 从上下文获取Service信息
    final Service service = WebUtils.getService(context);
    // 如果请求的"renew"属性存在,且TGT信息和Service信息都存在,才认为该请求用来获取ST
    // login请求则不满足该判定条件
    return (StringUtils.isNotBlank(context.getRequestParameters().get(CasProtocolConstants.PARAMETER_RENEW))
            && ticketGrantingTicketId != null
            && service != null);
}

protected Event createTicketGrantingTicket(final RequestContext context, final Credential credential,
                                           final MessageContext messageContext) {
    try {
        final Service service = WebUtils.getService(context);
        final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder(
                this.authenticationSystemSupport.getPrincipalElectionStrategy());
        // 包装页面提交的登录参数
        final AuthenticationTransaction transaction =
                AuthenticationTransaction.wrap(credential);
        // 鉴权登录参数
        this.authenticationSystemSupport.getAuthenticationTransactionManager().handle(transaction,  builder);
        final AuthenticationContext authenticationContext = builder.build(service);

        // 校验通过后,创建TGT
        final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationContext);
        // 将TGT放入上下文
        WebUtils.putTicketGrantingTicketInScopes(context, tgt);
        WebUtils.putWarnCookieIfRequestParameterPresent(this.warnCookieGenerator, context);
        putPublicWorkstationToFlowIfRequestParameterPresent(context);
        if (addWarningMessagesToMessageContextIfNeeded(tgt, messageContext)) {
            return newEvent(SUCCESS_WITH_WARNINGS);
        }
        return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_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(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e);
    }
}

核心逻辑主要如下:

  1. 包装登录参数;
  2. 鉴权登录参数;
  3. 创建TGT;

1 包装登录参数
其实就是将有效的Credential包装成AuthenticationTransaction,代码如下:

public static AuthenticationTransaction wrap(final Credential... credentials) {
    return new AuthenticationTransaction(sanitizeCredentials(credentials));
}

private static Set<Credential> sanitizeCredentials(final Credential[] credentials) {
    if (credentials != null && credentials.length > 0) {
        final Set<Credential> set = new HashSet<>(Arrays.asList(credentials));
        final Iterator<Credential> it = set.iterator();
        while (it.hasNext()) {
            if (it.next() == null) {
                // 过滤无效值
                it.remove();
            }
        }
        return set;
    }
    return Collections.emptySet();
}

2 鉴权登录参数
借助PolicyBasedAuthenticationManager实现鉴权,并将鉴权结果存入AuthenticationContextBuilder,代码如下:

public AuthenticationTransactionManager handle(final AuthenticationTransaction authenticationTransaction,
                                               final AuthenticationContextBuilder authenticationContext)
        throws AuthenticationException {
    if (!authenticationTransaction.getCredentials().isEmpty()) {
        // 借助PolicyBasedAuthenticationManager完成鉴权
        final Authentication authentication = this.authenticationManager.authenticate(authenticationTransaction);
        LOGGER.debug("Successful authentication; Collecting authentication result [{}]", authentication);
        // 收集鉴权结果
        authenticationContext.collect(authentication);
    }
    LOGGER.debug("Transaction ignored since there are no credentials to authenticate");
    return this;
}

PolicyBasedAuthenticationManager实现了AuthenticationManager接口,核心代码如下:

public Authentication authenticate(final AuthenticationTransaction transaction) throws AuthenticationException {

    // 鉴权
    final AuthenticationBuilder builder = authenticateInternal(transaction.getCredentials());
    final Authentication authentication = builder.build();
    final Principal principal = authentication.getPrincipal();
    if (principal instanceof NullPrincipal) {
        // 鉴权失败
        throw new UnresolvedPrincipalException(authentication);
    }

    // 添加鉴权方法属性,即AuthenticationHandler
    addAuthenticationMethodAttribute(builder, authentication);

    logger.info("Authenticated {} with credentials {}.", principal, transaction.getCredentials());
    logger.debug("Attribute map for {}: {}", principal.getId(), principal.getAttributes());

    // 填充鉴权元数据属性
    populateAuthenticationMetadataAttributes(builder, transaction.getCredentials());

    // 构建Authentication
    return builder.build();
}

protected AuthenticationBuilder authenticateInternal(final Collection<Credential> credentials)
        throws AuthenticationException {

    // 初始构造器
    final AuthenticationBuilder builder = new DefaultAuthenticationBuilder(NullPrincipal.getInstance());
    // 填充Credential数据
    for (final Credential c : credentials) {
        builder.addCredential(new BasicCredentialMetaData(c));
    }
    boolean found;

    // 遍历Credential
    for (final Credential credential : credentials) {
        found = false;
        // 遍历配置的AuthenticationHandler和PrincipalResolver MAP
        for (final Map.Entry<AuthenticationHandler, PrincipalResolver> entry : this.handlerResolverMap.entrySet()) {
            final AuthenticationHandler handler = entry.getKey();
            // 当前AuthenticationHandler是否支持处理当前Credential
            if (handler.supports(credential)) {
                // 存在处理当前Credential的AuthenticationHandler
                found = true;
                try {
                    // 鉴权并解析Principal
                    authenticateAndResolvePrincipal(builder, credential, entry.getValue(), handler);
                    // 判定是否满足退出条件
                    if (this.authenticationPolicy.isSatisfiedBy(builder.build())) {
                        return builder;
                    }
                } catch (final GeneralSecurityException e) {
                    logger.info("{} failed authenticating {}", handler.getName(), credential);
                    logger.debug("{} exception details: {}", handler.getName(), e.getMessage());
                    builder.addFailure(handler.getName(), e.getClass());
                } catch (final PreventedException e) {
                    logger.error("{}: {}  (Details: {})", handler.getName(), e.getMessage(), e.getCause().getMessage());
                    builder.addFailure(handler.getName(), e.getClass());
                }
            }
        }
        if (!found) {
            logger.warn(
                    "Cannot find authentication handler that supports [{}] of type [{}], which suggests a configuration problem.",
                    credential, credential.getClass().getSimpleName());
        }
    }
    // 校验生成的鉴权上下文
    // 如果无鉴权成功的HandlerResult或者不满足AuthenticationPolicy的条件,则抛出对应异常
    evaluateProducedAuthenticationContext(builder);

    return builder;
}

核心逻辑主要如下:

  1. 遍历Credential集合,针对每个Credential进行鉴权;
  2. 针对当前Credential,遍历配置的handlerResolverMap,找到匹配的AuthenticationHandler
  3. 根据匹配到的AuthenticationHandlerPrincipalResolver对当前Credential进行鉴权;
    1. 若鉴权成功,则根据AuthenticationPolicy的条件,判定是否结束鉴权;
      1. 结束,直接返回鉴权结果;
      2. 不结束,则继续下一轮AuthenticationHandlerPrincipalResolver,再继续下一轮Credential
    2. 若鉴权失败,AuthenticationBuilder记录失败信息;
  4. 遍历完Credential集合后,校验AuthenticationBuilder

2.1 遍历Credential集合
登录请求处理流程中,Credential集合仅存在一个Credential

2.2 遍历配置的handlerResolverMap
handlerResolverMap的配置信息如下:

<!-- deployerConfigContext.xml -->
<util:map id="authenticationHandlersResolvers">
    <entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
    <entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
</util:map>

<alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />

acceptUsersAuthenticationHandler对应的是AcceptUsersAuthenticationHandler,其匹配逻辑继承自父类AbstractUsernamePasswordAuthenticationHandler,代码如下:

public boolean supports(final Credential credential) {
    // 是否是UsernamePasswordCredential类型,
    return credential instanceof UsernamePasswordCredential;
}

proxyAuthenticationHandler对应的是HttpBasedServiceCredentialsAuthenticationHandler,其匹配逻辑如下:

public boolean supports(final Credential credential) {
    return credential instanceof HttpBasedServiceCredential;
}
<-- login-webflow.xml -->
<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>

credential定义配置可知,AcceptUsersAuthenticationHandler会处理本次login请求提交的CredentialAcceptUsersAuthenticationHandler不会处理本次login请求提交的Credential

2.3 鉴权Credential
AuthenticationHandlerPrincipalResolver对当前Credential进行鉴权,代码如下:

private void authenticateAndResolvePrincipal(final AuthenticationBuilder builder, final Credential credential,
                                             final PrincipalResolver resolver, final AuthenticationHandler handler)
        throws GeneralSecurityException, PreventedException {

    Principal principal;
    // 由AuthenticationHandler完成鉴权
    final HandlerResult result = handler.authenticate(credential);
    // 鉴权成功,记录AuthenticationHandler信息
    builder.addSuccess(handler.getName(), result);
    logger.info("{} successfully authenticated {}", handler.getName(), credential);

    if (resolver == null) {
        // 解析器为空,则Principal取自HandlerResult
        principal = result.getPrincipal();
        logger.debug(
                "No resolver configured for {}. Falling back to handler principal {}",
                handler.getName(),
                principal);
    } else {
        // 解析Principal
        principal = resolvePrincipal(handler.getName(), resolver, credential);
        
        if (principal == null) {
            logger.warn("Principal resolution handled by {} produced a null principal. "
                       + "This is likely due to misconfiguration or missing attributes; CAS will attempt to use the principal "
                       + "produced by the authentication handler, if any.", resolver.getClass().getSimpleName());
            // 解析无果,则Principal取自HandlerResult
            principal = result.getPrincipal();
        }
    }
    // Must avoid null principal since AuthenticationBuilder/ImmutableAuthentication
    // require principal to be non-null
    if (principal != null) {
        // 设置Principal
        builder.setPrincipal(principal);
    }
    
    logger.debug("Final principal resolved for this authentication event is {}", principal);
}

先由AuthenticationHandlerAcceptUsersAuthenticationHandler对当前Credential进行鉴权处理,其父类AbstractPreAndPostProcessingAuthenticationHandler实现了authenticate的算法模板,并提供preAuthenticatepostAuthenticate算法细节的默认实现,可由子类重写,提供doAuthentication算法细节,由子类实现,代码如下:

public final HandlerResult authenticate(final Credential credential)
        throws GeneralSecurityException, PreventedException {
    // 鉴权预处理
    if (!preAuthenticate(credential)) {
        throw new FailedLoginException();
    }

    // 鉴权后处理
    return postAuthenticate(credential, doAuthentication(credential));
}

protected boolean preAuthenticate(final Credential credential) {
    return true;
}

protected HandlerResult postAuthenticate(final Credential credential, final HandlerResult result) {
    return result;
}

protected abstract HandlerResult doAuthentication(Credential credential)
        throws GeneralSecurityException, PreventedException;

// 提供创建HandlerResult的接口供子类实现调用
protected final HandlerResult createHandlerResult(final Credential credential, final Principal principal,
                                                  final List<MessageDescriptor> warnings) {
    return new DefaultHandlerResult(this, new BasicCredentialMetaData(credential), principal, warnings);
}

AcceptUsersAuthenticationHandlerde的父类AbstractUsernamePasswordAuthenticationHandler实现了抽象的doAuthentication的算法细节的算法模板,并提供authenticateUsernamePasswordInternal算法细节,由子类实现,其代码如下:

protected final HandlerResult doAuthentication(final Credential credential)
        throws GeneralSecurityException, PreventedException {
    final UsernamePasswordCredential userPass = (UsernamePasswordCredential) credential;
    if (userPass.getUsername() == null) {
        // 无用户名则抛出异常
        throw new AccountNotFoundException("Username is null.");
    }
    
    // 支持转换用户名的策略模式
    // 默认使用NoOpPrincipalNameTransformer,不转换
    final String transformedUsername= this.principalNameTransformer.transform(userPass.getUsername());
    if (transformedUsername == null) {
        throw new AccountNotFoundException("Transformed username is null.");
    }
    // 更新为转换后的用户名
    userPass.setUsername(transformedUsername);
    // 鉴权转换后的数据
    return authenticateUsernamePasswordInternal(userPass);
}

protected abstract HandlerResult authenticateUsernamePasswordInternal(UsernamePasswordCredential transformedCredential)
        throws GeneralSecurityException, PreventedException;

AcceptUsersAuthenticationHandlerde实现了authenticateUsernamePasswordInternal算法细节,代码如下:

private static final String DEFAULT_SEPARATOR = "::";
private static final Pattern USERS_PASSWORDS_SPLITTER_PATTERN = Pattern.compile(DEFAULT_SEPARATOR);

private Map<String, String> users;

@Value("${accept.authn.users:}")
private String acceptedUsers;

@PostConstruct
public void init() {
    if (StringUtils.isNotBlank(this.acceptedUsers) && this.users == null) {
        // 若配置的用户信息(由cas.properties配置)存在,则当前缓存的用户信息不存在,则使用配置的用户信息
        // 配置信息格式:username::password
        final Set<String> usersPasswords = org.springframework.util.StringUtils.commaDelimitedListToSet(this.acceptedUsers);
        final Map<String, String> parsedUsers = new HashMap<>();
        for (final String usersPassword : usersPasswords) {
            final String[] splitArray = USERS_PASSWORDS_SPLITTER_PATTERN.split(usersPassword);
            parsedUsers.put(splitArray[0], splitArray[1]);
        }
        setUsers(parsedUsers);
    }
}

protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential)
        throws GeneralSecurityException, PreventedException {

    if (users == null || users.isEmpty()) {
        // 当前用户信息不存在,则不支持登录
        throw new FailedLoginException("No user can be accepted because none is defined");
    }
    final String username = credential.getUsername();
    final String cachedPassword = this.users.get(username);


    if (cachedPassword == null) {
        // 用户对应的密码不存在,则账户不存在
       logger.debug("{} was not found in the map.", username);
       throw new AccountNotFoundException(username + " not found in backing map.");
    }

    // 加密提交的密码信息
    // 默认使用PlainTextPasswordEncoder,即无加密
    final String encodedPassword = this.getPasswordEncoder().encode(credential.getPassword());
    // 对比密码信息
    if (!cachedPassword.equals(encodedPassword)) {
        throw new FailedLoginException();
    }
    // 密码校验通过,则创建HandlerResult
    // 默认使用DefaultPrincipalFactory创建SimplePrincipal,其属性为空MAP
    return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null);
}

public final void setUsers(@NotNull final Map<String, String> users) {
    this.users = Collections.unmodifiableMap(users);
}

用户信息由两种方式传入:

  1. 通过setUsers方式注入,优先级高;
<!-- deployerConfigContext.xml -->
<bean id="acceptUsersAuthenticationHandler"
      class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
    <property name="users">
        <map>
            <entry key="casuser" value="Mellon"/>
        </map>
    </property>
</bean>
  1. 通过cas.properties配置,优先级低;
accept.authn.users=casuser::Mellon

AuthenticationHandler鉴权失败,则抛异常结束;若AuthenticationHandler鉴权成功,则使用PrincipalResolver解析当前CredentialPrincipal信息,代码如下:

protected Principal resolvePrincipal(
        final String handlerName, final PrincipalResolver resolver, final Credential credential) {
    // 当前PrincipalResolver是否支持解析Credential
    if (resolver.supports(credential)) {
        try {
            // 解析当前Credential的Principal信息
            final Principal p = resolver.resolve(credential);
            logger.debug("{} resolved {} from {}", resolver, p, credential);
            return p;
        } catch (final Exception e) {
            logger.error("{} failed to resolve principal from {}", resolver, credential, e);
        }
    } else {
        logger.warn(
                "{} is configured to use {} but it does not support {}, which suggests a configuration problem.",
                handlerName,
                resolver,
                credential);
    }
    return null;
}

primaryPrincipalResolver的配置:

<!-- deployerConfigContext.xml -->
<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />

可知,personDirectoryPrincipalResolver对应的是PersonDirectoryPrincipalResolver,会处理本次login请求提交的Credential,代码如下:

/**
 * 本PrincipalResolver是否支持处理Credential
 */
public boolean supports(final Credential credential) {
    // 只要Credential的ID存在就支持
    return credential != null && credential.getId() != null;
}

/**
 * 解析Credential
 */
public Principal resolve(final Credential credential) {
    logger.debug("Attempting to resolve a principal...");

    final String principalId = extractPrincipalId(credential);

    // 校验principalId
    if (principalId == null) {
        logger.debug("Got null for extracted principal ID; returning null.");
        return null;
    }

    logger.debug("Creating SimplePrincipal for [{}]", principalId);

    // 获取principalId对应的属性集合
    final Map<String, List<Object>> attributes = retrievePersonAttributes(principalId, credential);

    // 校验属性集合
    if (attributes == null || attributes.isEmpty()) {
        logger.debug("Principal id [{}] did not specify any attributes", principalId);

        // 是否直接返回null
        if (!this.returnNullIfNoAttributes) {
            logger.debug("Returning the principal with id [{}] without any attributes", principalId);
            // 返回空属性Principal
            return this.principalFactory.createPrincipal(principalId);
        }
        logger.debug("[{}] is configured to return null if no attributes are found for [{}]",
                this.getClass().getName(), principalId);
        return null;
    }
    logger.debug("Retrieved [{}] attribute(s) from the repository", attributes.size());

    // 转换属性集合,可能会更新principalId参数,若配置了this.principalAttributeName属性
    final Pair<String, Map<String, Object>> pair = convertPersonAttributesToPrincipal(principalId, attributes);
    // 根据principalId和属性集合创建Principal
    return this.principalFactory.createPrincipal(pair.getFirst(), pair.getSecond());
}

protected String extractPrincipalId(final Credential credential) {
    return credential.getId();
}

protected Map<String, List<Object>> retrievePersonAttributes(final String principalId, final Credential credential) {
    // 从IPersonAttributeDao中查询用户属性
    final IPersonAttributes personAttributes = this.attributeRepository.getPerson(principalId);
    final Map<String, List<Object>> attributes;

    // 校验查询到的用户属性
    if (personAttributes == null) {
        attributes = null;
    } else {
        attributes = personAttributes.getAttributes();
    }
    return attributes;
}

protected Pair<String, Map<String, Object>> convertPersonAttributesToPrincipal(final String extractedPrincipalId,
                                                                               final Map<String, List<Object>> attributes) {
    final Map<String, Object> convertedAttributes = new HashMap<>();
    // 默认的principalId
    String principalId = extractedPrincipalId;
    for (final Map.Entry<String, List<Object>> entry : attributes.entrySet()) {
        final String key = entry.getKey();
        final List<Object> values = entry.getValue();
        if (StringUtils.isNotBlank(this.principalAttributeName)
                && key.equalsIgnoreCase(this.principalAttributeName)) {
            // 如果配置了this.principalAttributeName属性,则采用该属性值对应的第一个属性作为principalId
            if (values.isEmpty()) {
                logger.debug("{} is empty, using {} for principal", this.principalAttributeName, extractedPrincipalId);
            } else {
                // 更新principalId
                principalId = values.get(0).toString();
                logger.debug(
                        "Found principal attribute value {}; removing {} from attribute map.",
                        extractedPrincipalId,
                        this.principalAttributeName);
            }
        } else {
            // 保存属性值
            convertedAttributes.put(key, values.size() == 1 ? values.get(0) : values);
        }
    }

    return new Pair<>(principalId, convertedAttributes);
}

首先分析获取用户属性集合的方法:retrievePersonAttributes,该方法借助this.attributeRepository属性完成,该属性的赋值逻辑的核心代码如下:

// 默认使用StubPersonAttributeDao
protected IPersonAttributeDao attributeRepository = new StubPersonAttributeDao(new HashMap<String, List<Object>>());

// 可通过Setter注入自定义实现
public final void setAttributeRepository(@Qualifier("attributeRepository")
                                         final IPersonAttributeDao attributeRepository) {
    this.attributeRepository = attributeRepository;
}

attributeRepositorydeployerConfigContext.xml配置文件中有定义,配置如下:

<!-- deployerConfigContext.xml -->
<bean id="attributeRepository" class="org.jasig.services.persondir.support.NamedStubPersonAttributeDao"
      p:backingMap-ref="attrRepoBackingMap" />

<util:map id="attrRepoBackingMap">
    <entry key="uid" value="uid" />
    <entry key="eduPersonAffiliation" value="eduPersonAffiliation" />
    <entry key="groupMembership" value="groupMembership" />
    <entry>
        <key><value>memberOf</value></key>
        <list>
            <value>faculty</value>
            <value>staff</value>
            <value>org</value>
        </list>
    </entry>
</util:map>

NamedStubPersonAttributeDaogetPerson实现继承自父类StubPersonAttributeDao,代码如下:

public IPersonAttributes getPerson(String uid) {
  if (uid == null) {
    throw new IllegalArgumentException("Illegal to invoke getPerson(String) with a null argument.");
  } else {
    // 直接返回this.backingPerson,即配置文件中的"attrRepoBackingMap"
    return this.backingPerson;
  }
}

再分析convertPersonAttributesToPrincipal的逻辑,该方法中会根据已配置的this.principalAttributeName属性(若配置的话),从属性值对应的属性集合中取出第一个值更新principalId信息,该属性的赋值逻辑的核心代码如下:

// 可选属性值
protected String principalAttributeName;

// 可通过Setter注入自定义实现
public void setPrincipalAttributeName(@Value("${cas.principal.resolver.persondir.principal.attribute:}")
                                      final String attribute) {
    this.principalAttributeName = attribute;
}

该参数可配置在cas.properties文件中,默认值为:

#cas.principal.resolver.persondir.principal.attribute=cn

2.3.1 鉴权成功
鉴权成功后,会经过this.authenticationPolicy判断是否可以结束当前鉴权流程,该属性的赋值逻辑的核心代码如下:

// 默认是AnyAuthenticationPolicy,默认是只要有一个成功鉴权的Credential就满足
private AuthenticationPolicy authenticationPolicy = new AnyAuthenticationPolicy();

// 可通过Setter注入自定义实现
@Resource(name="authenticationPolicy")
public void setAuthenticationPolicy(final AuthenticationPolicy policy) {
    this.authenticationPolicy = policy;
}

分析下AuthenticationPolicyisSatisfiedBy的实现,代码如下:

// 默认false
private boolean tryAll;

// 可通过Setter注入自定义实现
public void setTryAll(@Value("${cas.authn.policy.any.tryall:false}") final boolean tryAll) {
    this.tryAll = tryAll;
}

public boolean isSatisfiedBy(final Authentication authn) {
    // 是否需要遍历全部,默认false,用户可通过cas.properties配置
    if (this.tryAll) {
        return authn.getCredentials().size() == authn.getSuccesses().size() + authn.getFailures().size();
    }
    // 只要有一个鉴权成功的Credential
    return !authn.getSuccesses().isEmpty();
}

2.3.2 鉴权失败
鉴权失败后,AuthenticationBuilder记录失败信息,然后继续下一轮AuthenticationHandlerPrincipalResolver,再继续下一轮Credential

2.4 校验AuthenticationBuilder
遍历完Credential集合后,正常退出前,会校验AuthenticationBuilder的有效性,如果没有鉴权成功的Credential,或者不满足this.authenticationPolicy的结束条件,则抛出异常,代码如下:

private void evaluateProducedAuthenticationContext(final AuthenticationBuilder builder) throws AuthenticationException {
    // We apply an implicit security policy of at least one successful authentication
    if (builder.getSuccesses().isEmpty()) {
        throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
    }
    // Apply the configured security policy
    if (!this.authenticationPolicy.isSatisfiedBy(builder.build())) {
        throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
    }
}

authenticateInternal鉴权结束后,会再次校验鉴权结果的Principal的有效性(鉴权过程中可能会返回NullPrincipal),然后添加AuthenticationHandler的信息,再经过配置的this.authenticationMetaDataPopulators,填充元数据属性,最后返回鉴权结果;
至此,鉴权流程结束;

3 创建TGT
鉴权成功后,就会使用this.centralAuthenticationService属性创建TGT信息,该属性的赋值逻辑的核心代码如下:

@Autowired
@Qualifier("centralAuthenticationService")
private CentralAuthenticationService centralAuthenticationService;

centralAuthenticationService对应的是CentralAuthenticationServiceImpl,其创建TGT的代码如下:

public TicketGrantingTicket createTicketGrantingTicket(final AuthenticationContext context)
        throws AuthenticationException, AbstractTicketException {

    // 获取鉴权结果
    final Authentication authentication = context.getAuthentication();
    // 获取TGT创建工厂
    final TicketGrantingTicketFactory factory = this.ticketFactory.get(TicketGrantingTicket.class);
    // 根据鉴权结果创建TGT
    final TicketGrantingTicket ticketGrantingTicket = factory.create(authentication);

    // 缓存创建的TGT
    this.ticketRegistry.addTicket(ticketGrantingTicket);

    // 发布TGT创建事件
    doPublishEvent(new CasTicketGrantingTicketCreatedEvent(this, ticketGrantingTicket));

    return ticketGrantingTicket;
}

this.ticketFactory属性保存了所欲配置的TicketFactory信息,该属性的赋值逻辑的核心代码如下:

@Resource(name="defaultTicketFactory")
protected TicketFactory ticketFactory;

defaultTicketFactory对应的是DefaultTicketFactory,核心代码是:

// 保存各种TicketFactory的实例
private Map<String, Object> factoryMap;

@Autowired
@Qualifier("defaultProxyTicketFactory")
private ProxyTicketFactory proxyTicketFactory;

@Autowired
@Qualifier("defaultServiceTicketFactory")
private ServiceTicketFactory serviceTicketFactory;

@Autowired
@Qualifier("defaultTicketGrantingTicketFactory")
private TicketGrantingTicketFactory ticketGrantingTicketFactory;

@Autowired
@Qualifier("defaultProxyGrantingTicketFactory")
private ProxyGrantingTicketFactory proxyGrantingTicketFactory;

@PostConstruct
public void initialize() {
    this.factoryMap = new HashMap<>();

    validateFactoryInstances();

    this.factoryMap.put(ProxyGrantingTicket.class.getCanonicalName(), this.proxyGrantingTicketFactory);
    this.factoryMap.put(TicketGrantingTicket.class.getCanonicalName(), this.ticketGrantingTicketFactory);
    this.factoryMap.put(ServiceTicket.class.getCanonicalName(), this.serviceTicketFactory);
    this.factoryMap.put(ProxyTicket.class.getCanonicalName(), this.proxyTicketFactory);
}

public <T extends TicketFactory> T get(final Class<? extends Ticket> clazz) {
    validateFactoryInstances();

    // 根据class从缓存的factoryMap中找到对应的实例
    return (T) this.factoryMap.get(clazz.getCanonicalName());
}

通过this.ticketFactory.get(TicketGrantingTicket.class)可以找到defaultTicketGrantingTicketFactory,即DefaultTicketGrantingTicketFactory,然后调用其create方法创建TGT,代码如下:

public <T extends TicketGrantingTicket> T create(final Authentication authentication) {
    final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
            this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(TicketGrantingTicket.PREFIX),
            authentication, ticketGrantingTicketExpirationPolicy);
    return (T) ticketGrantingTicket;
}

最终创建的TGT为TicketGrantingTicketImpl,其id值由this.ticketGrantingTicketUniqueTicketIdGenerator属性生成,该属性的赋值逻辑的核心代码如下:

@NotNull
@Resource(name="ticketGrantingTicketUniqueIdGenerator")
protected UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator;

ticketGrantingTicketUniqueIdGenerator对应的是TicketGrantingTicketIdGenerator,其getNewTicketId方法继承自父类DefaultUniqueTicketIdGenerator,代码如下:

public final String getNewTicketId(final String prefix) {
    final String number = this.numericGenerator.getNextNumberAsString();
    final StringBuilder buffer = new StringBuilder(prefix.length() + 2
            + (StringUtils.isNotBlank(this.suffix) ? this.suffix.length() : 0) + this.randomStringGenerator.getMaxLength()
            + number.length());

    buffer.append(prefix);
    buffer.append('-');
    buffer.append(number);
    buffer.append('-');
    buffer.append(this.randomStringGenerator.getNewString());

    if (this.suffix != null) {
        buffer.append(this.suffix);
    }

    return buffer.toString();
}

至此,TGT的创建流程结束;


成功创建出TGT后,就会走success处理,即gatewayRequestCheck,其配置如下:

<!-- login-webflow.xml -->
<action-state id="sendTicketGrantingTicket">
    <evaluate expression="sendTicketGrantingTicketAction"/>
    <transition to="serviceCheck"/>
</action-state>

sendTicketGrantingTicketAction对应的是SendTicketGrantingTicketAction,继承自AbstractAction,实现了算法细节doExecute,代码如下:

protected Event doExecute(final RequestContext context) {
    // 从上下文中获取TGT
    final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
    // 从cookie中获取TGT
    final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");

    if (ticketGrantingTicketId == null) {
        // 上下文中不存在TGT直接返回
        return success();
    }

    if (isAuthenticatingAtPublicWorkstation(context))  {
        // 通过公共工作台去认证的,将不产生cookie
        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 {
        LOGGER.debug("Setting TGC for current session.");
        // 设置TGT cookie
        this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils
            .getHttpServletResponse(context), ticketGrantingTicketId);
    }

    if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
        // 原cookie中的TGT和上下文中TGT不一样,则销毁原cookie中的TGT
        this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
    }

    return success();
}

销毁TGT由CentralAuthenticationServiceImpl实现,代码如下:

public List<LogoutRequest> destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) {
    try {
        logger.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId);
        // 根据TGT ID查询出TGT
        final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
        logger.debug("Ticket found. Processing logout requests and then deleting the ticket…");
        // 对使用该TGT的单点登出服务做登出操作
        final List<LogoutRequest> logoutRequests = logoutManager.performLogout(ticket);
        // 从缓存中删除TGT
        this.ticketRegistry.deleteTicket(ticketGrantingTicketId);

        // 发布TGT销毁事件
        doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket));

        return logoutRequests;
    } catch (final InvalidTicketException e) {
        logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
    }
    return Collections.emptyList();
}

logoutManager.performLogoutLogoutManagerImpl实现,代码如下:

public List<LogoutRequest> performLogout(final TicketGrantingTicket ticket) {
    // 获取当前TGT绑定的Service集合
    final Map<String, Service> services = ticket.getServices();
    final List<LogoutRequest> logoutRequests = new ArrayList<>();
    // if SLO is not disabled
    if (!this.singleLogoutCallbacksDisabled) {
        // 开启SLO功能
        // through all services
        for (final Map.Entry<String, Service> entry : services.entrySet()) {
            // it's a SingleLogoutService, else ignore
            final Service service = entry.getValue();
            if (service instanceof SingleLogoutService) {
                // 是单点登出服务则需要执行登出
                final LogoutRequest logoutRequest = handleLogoutForSloService((SingleLogoutService) service, entry.getKey());
                if (logoutRequest != null) {
                    LOGGER.debug("Captured logout request [{}]", logoutRequest);
                    logoutRequests.add(logoutRequest);
                }
            }
        }
    }

    return logoutRequests;
}

private LogoutRequest handleLogoutForSloService(final SingleLogoutService singleLogoutService, final String ticketId) {

    // 服务是否已登出
    if (!singleLogoutService.isLoggedOutAlready()) {
        // 找到已注册的服务
        final RegisteredService registeredService = servicesManager.findServiceBy(singleLogoutService);
        
        // 判断服务是否支持单点登出
        if (serviceSupportsSingleLogout(registeredService)) {

            // 获取服务的登出URL
            final URL logoutUrl = determineLogoutUrl(registeredService, singleLogoutService);
            // 封装登出请求
            final DefaultLogoutRequest logoutRequest = new DefaultLogoutRequest(ticketId, singleLogoutService, logoutUrl);
            // 获取服务的登出类型
            final LogoutType type = registeredService.getLogoutType() == null
                    ? LogoutType.BACK_CHANNEL : registeredService.getLogoutType();

            // 根据服务的登出类型执行相应操作
            switch (type) {
                case BACK_CHANNEL:
                    // 尝试发送登出请求
                    if (performBackChannelLogout(logoutRequest)) {
                        logoutRequest.setStatus(LogoutRequestStatus.SUCCESS);
                    } else {
                        logoutRequest.setStatus(LogoutRequestStatus.FAILURE);
                        LOGGER.warn("Logout message not sent to [{}]; Continuing processing...", singleLogoutService.getId());
                    }
                    break;
                default:
                    // 其他类型,则不支持反向通道登出操作
                    logoutRequest.setStatus(LogoutRequestStatus.NOT_ATTEMPTED);
                    break;
            }
            return logoutRequest;
        }
    }
    return null;
}

private boolean serviceSupportsSingleLogout(final RegisteredService registeredService) {
    return registeredService != null
            && registeredService.getAccessStrategy().isServiceAccessAllowed()
            && registeredService.getLogoutType() != LogoutType.NONE;
}

private URL determineLogoutUrl(final RegisteredService registeredService, final SingleLogoutService singleLogoutService) {
    try {
        URL logoutUrl = new URL(singleLogoutService.getOriginalUrl());
        final URL serviceLogoutUrl = registeredService.getLogoutUrl();

        if (serviceLogoutUrl != null) {
            LOGGER.debug("Logout request will be sent to [{}] for service [{}]",
                    serviceLogoutUrl, singleLogoutService);
            // 优先使用已注册服务的登出URL
            logoutUrl = serviceLogoutUrl;
        }
        return logoutUrl;
    } catch (final Exception e) {
        throw new IllegalArgumentException(e);
    }
}

private boolean performBackChannelLogout(final LogoutRequest request) {
    try {
        final String logoutRequest = this.logoutMessageBuilder.create(request);
        final SingleLogoutService logoutService = request.getService();
        // 设置服务的登出标识
        logoutService.setLoggedOutAlready(true);

        LOGGER.debug("Sending logout request for: [{}]", logoutService.getId());
        final LogoutHttpMessage msg = new LogoutHttpMessage(request.getLogoutUrl(), logoutRequest);
        LOGGER.debug("Prepared logout message to send is [{}]", msg);
        // 发送登出请求
        return this.httpClient.sendMessageToEndPoint(msg);
    } catch (final Exception e) {
        LOGGER.error(e.getMessage(), e);
    }
    return false;
}

login请求中cookie不会携带TGT信息,直接返回success(),走serviceCheck处理,其配置如下:

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

login请求中cookie不会携带Service信息,走viewGenericLoginSuccess处理,其配置如下:

<!-- login-webflow.xml -->
<end-state id="viewGenericLoginSuccess" view="casGenericSuccessView">
    <on-entry>
        <evaluate expression="genericSuccessViewAction.getAuthenticationPrincipal(flowScope.ticketGrantingTicketId)"
                  result="requestScope.principal"
                  result-type="org.jasig.cas.authentication.principal.Principal"/>
    </on-entry>
</end-state>

casGenericSuccessView对应的页面为/WEB-INF/view/jsp/default/ui/casGenericSuccessView.jsp
genericSuccessViewAction对应的是GenericSuccessViewAction,其getAuthenticationPrincipal方法的代码如下:

public Principal getAuthenticationPrincipal(final String ticketGrantingTicketId) {
    try {
        // 根据TGT ID获取TGT信息
        final TicketGrantingTicket ticketGrantingTicket =
                this.centralAuthenticationService.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
        // 返回TGT中鉴权结果的Principal信息
        return ticketGrantingTicket.getAuthentication().getPrincipal();
    } catch (final InvalidTicketException e){
        logger.warn(e.getMessage());
    }
    logger.debug("In the absence of valid TGT, the authentication principal cannot be determined. Returning {}",
            NullPrincipal.class.getSimpleName());
    // 异常情况返回空Principal
    return NullPrincipal.getInstance();
}

GenericSuccessViewActiongetAuthenticationPrincipal方法的返回结果会体现在页面/WEB-INF/view/jsp/default/ui/casGenericSuccessView.jsp上,该页面主要内容如下:

<div id="msg" class="success">
  <h2><spring:message code="screen.success.header" /></h2>
  <p><spring:message code="screen.success.success" arguments="${principal.id}"/></p>
  <p><spring:message code="screen.success.security" /></p>
</div>

至此,未携带Service的login请求的处理流程结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值