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的配置如下:

<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);
        // 默认为DefaultRegisteredServiceAccessStrategy,支持访问
        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");
}

针对login请求中携带的Service信息的处理如下:

  1. 从请求上下文中获取Service对象;
  2. Service对象存在,则
    1. 根据Service对象查找系统中注册的Service信息;
    2. 若已注册的Service信息存在且允许访问,则
      1. 保存已注册Service信息到请求上下文中;
      2. 若已注册Service信息存在未授权URL,则保存未授权URL到请求上下文中;
  3. Service对象不存在,则判断当前是否支持处理不携带Service的请求,
    1. 若不支持,则抛异常;
  4. 保存Service对象到请求上下文;
  5. 返回success

接下来重点分析:

  1. 从请求上下文中获取Service对象;
  2. 根据Service对象查找系统中注册的Service信息;

1 从请求上下文中获取Service对象
代码如下:

final Service service = WebUtils.getService(this.argumentExtractors, context);

@Resource(name="argumentExtractors")
public void setArgumentExtractors(final List<ArgumentExtractor> argumentExtractors) {
    this.argumentExtractors = argumentExtractors;
}


// WebUtils.java

public static WebApplicationService getService(
    final List<ArgumentExtractor> argumentExtractors,
    final RequestContext context) {
    final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
    return getService(argumentExtractors, request);
}

public static WebApplicationService getService(
    final List<ArgumentExtractor> argumentExtractors,
    final HttpServletRequest request) {
    // 遍历ArgumentExtractor集合
    for (final ArgumentExtractor argumentExtractor : argumentExtractors) {
        final WebApplicationService service = argumentExtractor
            .extractService(request);

        if (service != null) {
            return service;
        }
    }
    
    return null;
}

根据defaultArgumentExtractor的配置:

<!-- services-context.xml -->
<util:list id="argumentExtractors">
    <ref bean="defaultArgumentExtractor"/>
</util:list>

可知,defaultArgumentExtractor对应的是DefaultArgumentExtractor,其extractService继承自父类AbstractArgumentExtractorAbstractArgumentExtractor实现了extractService的算法模板,提供算法细节extractServiceInternal由子类实现实现,还提供了serviceFactoryList供子类实现使用,其代码如下:

public final WebApplicationService extractService(final HttpServletRequest request) {
    // 从request中抽取Service信息
    final WebApplicationService service = extractServiceInternal(request);

    // log
    if (service == null) {
        logger.debug("Extractor did not generate service.");
    } else {
        logger.debug("Extractor generated service for: {}", service.getId());
    }
    
    return service;
}

protected abstract WebApplicationService extractServiceInternal(HttpServletRequest request);

@Resource(name="serviceFactoryList")
protected List<ServiceFactory<? extends WebApplicationService>> serviceFactoryList;

protected final List<ServiceFactory<? extends WebApplicationService>> getServiceFactories() {
    return serviceFactoryList;
}

serviceFactoryList的配置如下:

<!-- services-context.xml -->
<util:list id="serviceFactoryList" value-type="org.jasig.cas.authentication.principal.ServiceFactory">
    <ref bean="webApplicationServiceFactory" />
</util:list>

DefaultArgumentExtractor实现了算法细节extractServiceInternal,代码如下:

public WebApplicationService extractServiceInternal(final HttpServletRequest request) {
    for (final ServiceFactory<? extends WebApplicationService> factory : getServiceFactories()) {
        final WebApplicationService service = factory.createService(request);
        if (service != null) {
            // 创建成功则直接返回
            logger.debug("Created {} based on {}", service, factory);
            return service;
        }
    }
    logger.debug("No service could be extracted based on the given request");
    return null;
}

webApplicationServiceFactory对应的是WebApplicationServiceFactory,其createService方法代码如下:

public WebApplicationService createService(final HttpServletRequest request) {
    final String targetService = request.getParameter(CasProtocolConstants.PARAMETER_TARGET_SERVICE);
    final String service = request.getParameter(CasProtocolConstants.PARAMETER_SERVICE);
    final String serviceAttribute = (String) request.getAttribute(CasProtocolConstants.PARAMETER_SERVICE);
    final String method = request.getParameter(CasProtocolConstants.PARAMETER_METHOD);
    final String format = request.getParameter(CasProtocolConstants.PARAMETER_FORMAT);

    final String serviceToUse;
    if (StringUtils.isNotBlank(targetService)) {
        // 优先使用targetService
        serviceToUse = targetService;
    } else if (StringUtils.isNotBlank(service)) {
        // 其次使用请求参数中的service 
        serviceToUse = service;
    } else {
        // 最后使用请求属性中的service
        serviceToUse = serviceAttribute;
    }
    
    // 校验service信息
    if (StringUtils.isBlank(serviceToUse)) {
        return null;
    }
    
    // 去除jsession信息
    final String id = AbstractServiceFactory.cleanupUrl(serviceToUse);
    // 获取请求参数中的ticket信息,并将其作为Service的artifactId
    final String artifactId = request.getParameter(CasProtocolConstants.PARAMETER_TICKET);
    
    final Response.ResponseType type = HttpMethod.POST.name().equalsIgnoreCase(method) ? Response.ResponseType.POST
            : Response.ResponseType.REDIRECT;
    
    // 创建SimpleWebApplicationServiceImpl
    final SimpleWebApplicationServiceImpl webApplicationService =
            new SimpleWebApplicationServiceImpl(id, serviceToUse,
                    artifactId, new WebApplicationServiceResponseBuilder(type));
    
    try {
        if (StringUtils.isNotBlank(format)) {
            // 若请求参数中存在format信息,则设置该属性
            final ValidationResponseType formatType = ValidationResponseType.valueOf(format.toUpperCase());
            webApplicationService.setFormat(formatType);
        }
    } catch (final Exception e) {
        logger.error("Format specified in the request [{}] is not recognized", format);
        return null;
    }
    return webApplicationService;
}

WebApplicationServiceFactory创建的ServiceSimpleWebApplicationServiceImpl,支持单点登出;

public final class SimpleWebApplicationServiceImpl extends AbstractWebApplicationService
public abstract class AbstractWebApplicationService implements SingleLogoutService
public interface SingleLogoutService extends WebApplicationService
public interface WebApplicationService extends Service

2 根据Service对象查找系统中注册的Service信息
代码如下:

final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

private ServicesManager servicesManager;

@Autowired
public void setServicesManager(@Qualifier("servicesManager") final ServicesManager servicesManager) {
    this.servicesManager = servicesManager;
}

servicesManager对应的是DefaultServicesManagerImplfindServiceBy方法的代码如下:

public RegisteredService findServiceBy(final Service service) {
    final Collection<RegisteredService> c = convertToTreeSet();

    // 遍历注册的Service
    for (final RegisteredService r : c) {
        if (r.matches(service)) {
            // 若匹配则返回该注册的Service
            return r;
        }
    }
    
    return null;
}

public TreeSet<RegisteredService> convertToTreeSet() {
    return new TreeSet<>(this.services.values());
}

private ConcurrentHashMap<Long, RegisteredService> services = new ConcurrentHashMap<>();

public void load() {
    final ConcurrentHashMap<Long, RegisteredService> localServices =
            new ConcurrentHashMap<>();

    // 借助this.serviceRegistryDao加载注册Service信息
    for (final RegisteredService r : this.serviceRegistryDao.load()) {
        LOGGER.debug("Adding registered service {}", r.getServiceId());
        localServices.put(r.getId(), r);
    }
    
    this.services = localServices;
    LOGGER.info("Loaded {} services from {}.", this.services.size(),
            this.serviceRegistryDao);
}

public DefaultServicesManagerImpl(@Qualifier("serviceRegistryDao") final ServiceRegistryDao serviceRegistryDao) {
    this.serviceRegistryDao = serviceRegistryDao;
    load();
}

ServiceRegistryDao接口有多种实现,可根据实际需求,在配置文件中自行配置该接口的实现;以InMemoryServiceRegistryDaoImpl为例,分析load方法实现,代码如下:

public List<RegisteredService> load() {
    return this.registeredServices;
}

@PostConstruct
public void afterPropertiesSet() {
    final String[] aliases =
        this.applicationContext.getAutowireCapableBeanFactory().getAliases("inMemoryServiceRegistryDao");
    // 如果配置了"inMemoryServiceRegistryDao"
    if (aliases.length > 0) {
        LOGGER.debug("{} is used as the active service registry dao", this.getClass().getSimpleName());

        try {
            // 从IOC容器中找到"inMemoryRegisteredServices"的配置
            final List<RegisteredService> list = (List<RegisteredService>)
                this.applicationContext.getBean("inMemoryRegisteredServices", List.class);
            if (list != null) {
                LOGGER.debug("Loaded {} services from the application context for {}",
                    list.size(),
                    this.getClass().getSimpleName());
                this.registeredServices = list;
            }
        } catch (final Exception e) {
            LOGGER.debug("No registered services are defined for {}", this.getClass().getSimpleName());
        }
    }
}

inMemoryRegisteredServices配置信息举例如下:

<util:list id="inMemoryRegisteredServices">
    <bean class="org.jasig.cas.services.RegexRegisteredService"
          p:id="0" p:name="HTTP and IMAP" p:description="Allows HTTP(S) and IMAP(S) protocols"
          p:serviceId="^(https?|imaps?)://.*" p:evaluationOrder="10000001" >
        <property name="attributeReleasePolicy">
            <bean class="org.jasig.cas.services.ReturnAllAttributeReleasePolicy" />
        </property>
    </bean>
</util:list>

常用的RegisteredService接口的实现类为RegexRegisteredService,支持正则表达式,其match方法实现代码如下:

public boolean matches(final Service service) {
    if (this.servicePattern == null) {
        this.servicePattern = RegexUtils.createPattern(this.serviceId);
    }
    // 根据serviceId的正则规则进行匹配
    return service != null && this.servicePattern != null
            && this.servicePattern.matcher(service.getId()).matches();
}

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>

根据表达式可知,不携带gateway信息的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信息能匹配已注册的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;
}

credential定义配置:

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

可知,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);
}

AcceptUsersAuthenticationHandler的父类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配置文件中有定义,配置如下:

<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信息,走generateServiceTicket处理,其配置如下:

<!-- login-webflow.xml -->
<action-state id="generateServiceTicket">
    <evaluate expression="generateServiceTicketAction"/>
    <transition on="success" to="warn"/>
    <transition on="authenticationFailure" to="handleAuthenticationFailure"/>
    <transition on="error" to="initializeLogin"/>
    <transition on="gateway" to="gatewayServicesManagementCheck"/>
</action-state>

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

protected Event doExecute(final RequestContext context) {
    // 从上下文中获取Service信息
    final Service service = WebUtils.getService(context);
    // 从上下文中获取TGT信息
    final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);

    try {
        /**
         * In the initial primary authentication flow, credentials are cached and available.
         * Since they are authenticated as part of submission first, there is no need to doubly
         * authenticate and verify credentials.
         *
         * In subsequent authentication flows where a TGT is available and only an ST needs to be
         * created, there are no cached copies of the credential, since we do have a TGT available.
         * So we will simply grab the available authentication and produce the final result based on that.
         */
        // 根据TGT获取对应的鉴权结果
        final Authentication authentication = ticketRegistrySupport.getAuthenticationFrom(ticketGrantingTicket);
        if (authentication == null) {
            // TGT无对应的鉴权结果则无效
            throw new InvalidTicketException(new AuthenticationException(), ticketGrantingTicket);
        }
    
        final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder(
                this.authenticationSystemSupport.getPrincipalElectionStrategy());
        final AuthenticationContext authenticationContext = builder.collect(authentication).build(service);
    
        // 根据TGT,Service等创建ST
        final ServiceTicket serviceTicketId = this.centralAuthenticationService
                .grantServiceTicket(ticketGrantingTicket, service, authenticationContext);
        WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
        return success();
    } catch (final AuthenticationException e) {
        logger.error("Could not verify credentials to grant service ticket", e);
    } catch (final AbstractTicketException e) {
        if (e instanceof InvalidTicketException) {
            // 销毁TGT
            this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
        }
        if (isGatewayPresent(context)) {
            return result("gateway");
        }
    
        return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e);
    }
    
    return error();
}

this.centralAuthenticationService就是CentralAuthenticationServiceImpl,在创建TGT时介绍过,这里介绍其grantServiceTicket方法,代码如下:

public ServiceTicket grantServiceTicket(
        final String ticketGrantingTicketId,
        final Service service, final AuthenticationContext context)
        throws AuthenticationException, AbstractTicketException {

    // 根据TGT ID获取缓存的TGT信息
    final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
    // 根据Service信息匹配获取已注册的Service信息
    final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
    
    // 校验已注册Service信息
    verifyRegisteredServiceProperties(registeredService, service);
    // 校验当前鉴权结果和TGT中缓存的鉴权结果是否一致
    final Authentication currentAuthentication = evaluatePossibilityOfMixedPrincipals(context, ticketGrantingTicket);
    
    // 校验TGT的使用记录和已注册Service的访问策略
    if (ticketGrantingTicket.getCountOfUses() > 0 && !registeredService.getAccessStrategy().isServiceAccessAllowedForSso()) {
        // 如果TGT已经绑定过ST且当前已注册的Service不支持SSO,则抛异常
        logger.warn("Service [{}] is not allowed to use SSO.", service.getId());
        throw new UnauthorizedSsoServiceException();
    }
    
    // 校验代理Service如有必要
    // 本例不涉及
    evaluateProxiedServiceIfNeeded(service, ticketGrantingTicket, registeredService);


    // Perform security policy check by getting the authentication that satisfies the configured policy
    // This throws if no suitable policy is found
    // 获取满足安全策略的鉴权结果
    // 默认使用AcceptAnyAuthenticationPolicyFactory,默认满足
    getAuthenticationSatisfiedByPolicy(ticketGrantingTicket.getRoot(), new ServiceContext(service, registeredService));
    
    // 获取TGT缓存的鉴权结果,以及TGT关联的ST的缓存的鉴权结果
    final List<Authentication> authentications = ticketGrantingTicket.getChainedAuthentications();
    // 获取最后一个鉴权结果的Principal
    final Principal principal = authentications.get(authentications.size() - 1).getPrincipal();
    
    // 默认为空
    final RegisteredServiceAttributeReleasePolicy releasePolicy = registeredService.getAttributeReleasePolicy();
    final Map<String, Object> principalAttrs;
    if (releasePolicy != null) {
        principalAttrs = releasePolicy.getAttributes(principal);
    } else {
        principalAttrs = new HashMap<>();
    }
    
    // 校验principal是否满足RegisteredServiceAttributeReleasePolicy的要求,默认principalAttrs为空,故满足
    if (!registeredService.getAccessStrategy().doPrincipalAttributesAllowServiceAccess(principal.getId(), principalAttrs)) {
        logger.warn("Cannot grant service ticket because Service [{}] is not authorized for use by [{}].",
                service.getId(), principal);
        throw new UnauthorizedServiceForPrincipalException();
    }
    
    // 获取ServiceTicketFactory实例,即DefaultServiceTicketFactory
    final ServiceTicketFactory factory = this.ticketFactory.get(ServiceTicket.class);
    // 根据TGT,Service创建ST
    final ServiceTicket serviceTicket = factory.create(ticketGrantingTicket, service, currentAuthentication != null);
    // 缓存ST
    this.ticketRegistry.addTicket(serviceTicket);
    
    logger.info("Granted ticket [{}] for service [{}] and principal [{}]",
            serviceTicket.getId(), service.getId(), principal.getId());
    
    // 发布ST创建事件
    doPublishEvent(new CasServiceTicketGrantedEvent(this, ticketGrantingTicket, serviceTicket));
    
    // 返回ST
    return serviceTicket;
}

protected final void verifyRegisteredServiceProperties(final RegisteredService registeredService,
                                                       final Service service) throws UnauthorizedServiceException {
    if (registeredService == null) {
        final String msg = String.format("ServiceManagement: 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("ServiceManagement: Unauthorized Service Access. "
                + "Service [%s] is not enabled in service registry.", service.getId());


        logger.warn(msg);
        throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
    }
}

private static Authentication evaluatePossibilityOfMixedPrincipals(final AuthenticationContext context,
                                                            final TicketGrantingTicket ticketGrantingTicket)
        throws MixedPrincipalException {
    Authentication currentAuthentication = null;
    if (context != null) {
        currentAuthentication = context.getAuthentication();
        if (currentAuthentication != null) {
            final Authentication original = ticketGrantingTicket.getAuthentication();
            // 校验当前鉴权结果和TGT中缓存的鉴权结果是否一致
            if (!currentAuthentication.getPrincipal().equals(original.getPrincipal())) {
                throw new MixedPrincipalException(
                        currentAuthentication, currentAuthentication.getPrincipal(), original.getPrincipal());
            }
            ticketGrantingTicket.getSupplementalAuthentications().clear();
            ticketGrantingTicket.getSupplementalAuthentications().add(currentAuthentication);
        }
    }
    return currentAuthentication;
}

protected final void evaluateProxiedServiceIfNeeded(final Service service, final TicketGrantingTicket ticketGrantingTicket,
                                            final RegisteredService registeredService) {
    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.trace("TGT is not proxied by another service");
    }
}

protected final Authentication getAuthenticationSatisfiedByPolicy(
        final TicketGrantingTicket ticket, final ServiceContext context) throws AbstractTicketException {

    final ContextualAuthenticationPolicy<ServiceContext> policy =
            serviceContextAuthenticationPolicyFactory.createPolicy(context);
    if (policy.isSatisfiedBy(ticket.getAuthentication())) {
        return ticket.getAuthentication();
    }
    for (final Authentication auth : ticket.getSupplementalAuthentications()) {
        if (policy.isSatisfiedBy(auth)) {
            return auth;
        }
    }
    throw new UnsatisfiedAuthenticationPolicyException(policy);
}


// AcceptAnyAuthenticationPolicyFactory.java

public ContextualAuthenticationPolicy<ServiceContext> createPolicy(final ServiceContext context) {
    return new ContextualAuthenticationPolicy<ServiceContext>() {

        @Override
        public ServiceContext getContext() {
            return context;
        }
    
        @Override
        public boolean isSatisfiedBy(final Authentication authentication) {
            return true;
        }
    };
}

DefaultServiceTicketFactorycreate方法,代码如下:

public <T extends Ticket> T create(final TicketGrantingTicket ticketGrantingTicket,
                                   final Service service,
                                   final boolean credentialsProvided) {

    final String uniqueTicketIdGenKey = service.getClass().getName();
    UniqueTicketIdGenerator serviceTicketUniqueTicketIdGenerator = null;
    // 尝试获取Service特定的UniqueTicketIdGenerator
    if (this.uniqueTicketIdGeneratorsForService != null && !uniqueTicketIdGeneratorsForService.isEmpty()) {
        logger.debug("Looking up service ticket id generator for [{}]", uniqueTicketIdGenKey);
        serviceTicketUniqueTicketIdGenerator = this.uniqueTicketIdGeneratorsForService.get(uniqueTicketIdGenKey);
    }
    if (serviceTicketUniqueTicketIdGenerator == null) {
        // 若Service不存在特定的UniqueTicketIdGenerator,则使用默认
        serviceTicketUniqueTicketIdGenerator = this.defaultServiceTicketIdGenerator;
        logger.debug("Service ticket id generator not found for [{}]. Using the default generator...",
                uniqueTicketIdGenKey);
    }
    
    // 生成ST ID
    final String ticketId = serviceTicketUniqueTicketIdGenerator.getNewTicketId(ServiceTicket.PREFIX);
    // 借助TGT创建ST
    final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket(
            ticketId,
            service,
            this.serviceTicketExpirationPolicy,
            credentialsProvided,
            this.onlyTrackMostRecentSession);
    return (T) serviceTicket;
}

TGT是TicketGrantingTicketImpl类型的,其grantServiceTicket方法的代码如下:

public final synchronized ServiceTicket grantServiceTicket(final String id,
    final Service service, final ExpirationPolicy expirationPolicy,
    final boolean credentialsProvided, final boolean onlyTrackMostRecentSession) {

    // 创建ST
    final ServiceTicket serviceTicket = new ServiceTicketImpl(id, this,
            service, this.getCountOfUses() == 0 || credentialsProvided,
            expirationPolicy);
    
    // 
    updateServiceAndTrackSession(serviceTicket.getId(), service, onlyTrackMostRecentSession);
    return serviceTicket;
}

protected void updateServiceAndTrackSession(final String id, final Service service, final boolean onlyTrackMostRecentSession) {
    updateState();

    // 更新服务Principal信息
    final List<Authentication> authentications = getChainedAuthentications();
    service.setPrincipal(authentications.get(authentications.size()-1).getPrincipal());
    
    // 如果开启跟踪最近访问会话,则需要清除缓存的该service信息若存在
    if (onlyTrackMostRecentSession) {
        final String path = normalizePath(service);
        final Collection<Service> existingServices = services.values();
        // loop on existing services
        for (final Service existingService : existingServices) {
            final String existingPath = normalizePath(existingService);
            // if an existing service has the same normalized path, remove it
            // and its service ticket to keep the latest one
            if (StringUtils.equals(path, existingPath)) {
                // 存在该service的缓存信息则需要清除
                existingServices.remove(existingService);
                LOGGER.trace("Removed previous tickets for service: {}", existingService);
                break;
            }
        }
    }
    // 缓存该service的信息
    this.services.put(id, service);
}

protected final void updateState() {
    // 更新记录
    this.previousLastTimeUsed = this.lastTimeUsed;
    this.lastTimeUsed = System.currentTimeMillis();
    this.countOfUses++;
}

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


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

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

根据表达式可知,若无warn信息,则走redirect,其配置如下:

<!-- login-webflow.xml -->
<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>

根据表达式可知,根据service获取带有ST ID信息Response,代码如下:

// AbstractWebApplicationService.java

public Response getResponse(final String ticketId) {
    return this.responseBuilder.build(this, ticketId);
}


// WebApplicationServiceResponseBuilder.java

public Response build(final WebApplicationService service, final String ticketId) {
    final Map<String, String> parameters = new HashMap<>();

    if (StringUtils.hasText(ticketId)) {
        // 保存ST ID信息
        parameters.put(CasProtocolConstants.PARAMETER_TICKET, ticketId);
    }
    
    if (responseType.equals(Response.ResponseType.POST)) {
        return buildPost(service, parameters);
    }
    if (responseType.equals(Response.ResponseType.REDIRECT)) {
        return buildRedirect(service, parameters);
    }
    
    throw new IllegalArgumentException("Response type is valid. Only POST/REDIRECT are supported");
}

创建出Response后,走postRedirectDecision,其配置如下:

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

根据表达式可知,若响应时POST类型,则走postView,否则走redirectView,本例走redirectView,其配置如下:

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

根据表达式可知,通过ExternalRedirectAction实现重定向,ExternalRedirectAction继承自AbstractAction,实现了算法细节doExecute,代码如下:

protected Event doExecute(RequestContext context) throws Exception {
  String resourceUri = (String)this.resourceUri.getValue(context);
  context.getExternalContext().requestExternalRedirect(resourceUri);
  return this.success();
}

重定向到login请求中携带的Service,并携带新创建的ST信息;

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值