CAS流程简析 服务端校验Ticket

相关阅读

简介

用户访问客户端的请求若携带Ticket信息,经过客户端配置的过滤器Cas30ProxyReceivingTicketValidationFilter时,该过滤器会将请求中携带的Ticket信息发送到服务端进行校验,若校验通过,才返回鉴权结果;

简析

Cas30ProxyReceivingTicketValidationFilter发送到服务端的请求路径为:/p3/serviceValidate,对应服务端的处理器为V3ServiceValidateController,核心代码如下:

@Component("v3ServiceValidateController")
@Controller
public class V3ServiceValidateController extends AbstractServiceValidateController {
    /**
     * Handle model and view.
     *
     * @param request the request
     * @param response the response
     * @return the model and view
     * @throws Exception the exception
     */
    @RequestMapping(path="/p3/serviceValidate", method = RequestMethod.GET)
    protected ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response)
        throws Exception {
        return super.handleRequestInternal(request, response);
    }

    @Override
    @Autowired
    public void setValidationSpecificationClass(@Value("org.jasig.cas.validation.Cas20WithoutProxyingValidationSpecification")
                                                final Class<?> validationSpecificationClass) {
        super.setValidationSpecificationClass(validationSpecificationClass);
    }

    @Override
    @Autowired
    public void setFailureView(@Value("cas3ServiceFailureView") final String failureView) {
        super.setFailureView(failureView);
    }

    @Override
    @Autowired
    public void setSuccessView(@Value("cas3ServiceSuccessView") final String successView) {
        super.setSuccessView(successView);
    }

    @Override
    @Autowired
    public void setProxyHandler(@Qualifier("proxy20Handler") final ProxyHandler proxyHandler) {
        super.setProxyHandler(proxyHandler);
    }
}

V3ServiceValidateController校验Ticket方法来源于父类AbstractServiceValidateControllerhandleRequestInternal方法,代码如下:

protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
        throws Exception {
    // 从请求中获取Service信息
    final WebApplicationService service = this.argumentExtractor.extractService(request);
    // 获取ST ID
    final String serviceTicketId = service != null ? service.getArtifactId() : null;

    // 校验Service和ST ID
    if (service == null || serviceTicketId == null) {
        logger.debug("Could not identify service and/or service ticket for service: [{}]", service);
        return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_REQUEST,
                CasProtocolConstants.ERROR_CODE_INVALID_REQUEST, null, request, service);
    }

    try {
        // 获取pgtUrl的鉴权结果
        // 代理模式涉及,本例不涉及
        TicketGrantingTicket proxyGrantingTicketId = null;
        final Credential serviceCredential = getServiceCredentialsFromRequest(service, request);
        if (serviceCredential != null) {
            proxyGrantingTicketId = handleProxyGrantingTicketDelivery(serviceTicketId, serviceCredential);
            if (proxyGrantingTicketId == null) {
                return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
                        CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
                        new Object[]{serviceCredential.getId()}, request, service);
            }
        }

        // 校验ST ID
        final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
        // 校验认证结果
        if (!validateAssertion(request, serviceTicketId, assertion)) {
            return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_TICKET,
                    CasProtocolConstants.ERROR_CODE_INVALID_TICKET, null, request, service);
        }

        // 获取proxyIou
        // 代理模式涉及,本例不涉及
        String proxyIou = null;
        if (serviceCredential != null && this.proxyHandler.canHandle(serviceCredential)) {
            proxyIou = this.proxyHandler.handle(serviceCredential, proxyGrantingTicketId);
            if (StringUtils.isEmpty(proxyIou)) {
                return generateErrorView(CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
                        CasProtocolConstants.ERROR_CODE_INVALID_PROXY_CALLBACK,
                        new Object[] {serviceCredential.getId()}, request, service);
            }
        }

        // 校验成功时处理
        onSuccessfulValidation(serviceTicketId, assertion);
        logger.debug("Successfully validated service ticket {} for service [{}]", serviceTicketId, service.getId());
        // 创建成功视图
        return generateSuccessView(assertion, proxyIou, service, proxyGrantingTicketId);
    } catch (final AbstractTicketValidationException e) {
        final String code = e.getCode();
        return generateErrorView(code, code,
                new Object[] {serviceTicketId, e.getOriginalService().getId(), service.getId()}, request, service);
    } catch (final AbstractTicketException te) {
        return generateErrorView(te.getCode(), te.getCode(),
            new Object[] {serviceTicketId}, request, service);
    } catch (final UnauthorizedProxyingException e) {
        return generateErrorView(e.getMessage(), e.getMessage(), new Object[] {service.getId()}, request, service);
    } catch (final UnauthorizedServiceException e) {
        return generateErrorView(e.getMessage(), e.getMessage(), null, request, service);
    }
}

主要逻辑如下:

  1. 获取并校验Service信息和ST ID;
  2. 获取pgtUrl;
  3. 校验ST ID;
  4. 校验认证结果;
  5. 获取proxyIou;
  6. 创建视图;

1 获取并校验Service信息和ST ID

首先分析从request中如何获取Service和ST ID,代码如下:

@Qualifier("defaultArgumentExtractor")
private ArgumentExtractor argumentExtractor;

final WebApplicationService service = this.argumentExtractor.extractService(request);
final String serviceTicketId = service != null ? service.getArtifactId() : null;

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 获取pgtUrl

根据请求中的pgtUrl信息获取对应的鉴权结果,代码如下:

protected Credential getServiceCredentialsFromRequest(final WebApplicationService service, final HttpServletRequest request) {
    // 获取请求中的"pgtUrl"参数
    final String pgtUrl = request.getParameter(CasProtocolConstants.PARAMETER_PROXY_CALLBACK_URL);
    if (StringUtils.hasText(pgtUrl)) {
        // pgtUrl参数存在
        try {
            // 获取对应的已注册Service信息
            final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
            // 校验已注册Service属性
            verifyRegisteredServiceProperties(registeredService, service);
            return new HttpBasedServiceCredential(new URL(pgtUrl), registeredService);
        } catch (final Exception e) {
            logger.error("Error constructing pgtUrl", e);
        }
    }

    return null;
}

代理模式涉及该处理,本例不涉及;

3 校验ST ID

从request请求中获取到ST ID,需要对其进行校验,代码如下:

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

@Qualifier("centralAuthenticationService")
private CentralAuthenticationService centralAuthenticationService;

centralAuthenticationService对应的是CentralAuthenticationServiceImpl,其validateServiceTicket方法代码如下:

public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws AbstractTicketException {
    // 根据Service信息获取已注册Service信息
    final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
    // 校验已注册Service属性
    verifyRegisteredServiceProperties(registeredService, service);

    // 根据ST ID获取ST
    final ServiceTicket serviceTicket =  this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);

    // 校验ST
    if (serviceTicket == null) {
        logger.info("Service ticket [{}] does not exist.", serviceTicketId);
        throw new InvalidTicketException(serviceTicketId);
    }

    try {
        synchronized (serviceTicket) {
            // ST是否过期
            if (serviceTicket.isExpired()) {
                logger.info("ServiceTicket [{}] has expired.", serviceTicketId);
                throw new InvalidTicketException(serviceTicketId);
            }

            // ST是否支持当前Service
            if (!serviceTicket.isValidFor(service)) {
                logger.error("Service ticket [{}] with service [{}] does not match supplied service [{}]",
                        serviceTicketId, serviceTicket.getService().getId(), service);
                throw new UnrecognizableServiceForServiceTicketValidationException(serviceTicket.getService());
            }
        }

        // 获取ST对应的TGT
        final TicketGrantingTicket root = serviceTicket.getGrantingTicket().getRoot();
        // 获取鉴权结果
        final Authentication authentication = getAuthenticationSatisfiedByPolicy(
                root, new ServiceContext(serviceTicket.getService(), registeredService));
        // 获取Principal
        final Principal principal = authentication.getPrincipal();

        final RegisteredServiceAttributeReleasePolicy attributePolicy = registeredService.getAttributeReleasePolicy();
        logger.debug("Attribute policy [{}] is associated with service [{}]", attributePolicy, registeredService);
        
        @SuppressWarnings("unchecked")
        final Map<String, Object> attributesToRelease = attributePolicy != null
                ? attributePolicy.getAttributes(principal) : Collections.EMPTY_MAP;
        
        final String principalId = registeredService.getUsernameAttributeProvider().resolveUsername(principal, service);
        final Principal modifiedPrincipal = this.principalFactory.createPrincipal(principalId, attributesToRelease);
        final AuthenticationBuilder builder = DefaultAuthenticationBuilder.newInstance(authentication);
        builder.setPrincipal(modifiedPrincipal);

        // 创建认证结果
        final Assertion assertion = new ImmutableAssertion(
                builder.build(),
                serviceTicket.getGrantingTicket().getChainedAuthentications(),
                serviceTicket.getService(),
                serviceTicket.isFromNewLogin());

        // 发布ST校验成功事件
        doPublishEvent(new CasServiceTicketValidatedEvent(this, serviceTicket, assertion));

        // 返回认证结果
        return assertion;
    } finally {
        if (serviceTicket.isExpired()) {
            this.ticketRegistry.deleteTicket(serviceTicketId);
        }
    }
}

主要逻辑如下:

  1. 根据Service信息获取已注册Service信息并校验;
  2. 根据ST ID获取ST;
  3. 校验ST;
  4. 创建认证结果;

3.1 根据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();
}

3.2 根据ST ID获取ST

代码如下:

final ServiceTicket serviceTicket =  this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);

@Resource(name="ticketRegistry")
protected TicketRegistry ticketRegistry;

TicketRegistry由用户配置,可以看下DefaultTicketRegistry的实现,getTicket方法由AbstractTicketRegistry实现了算法模板,代码如下:

public final <T extends Ticket> T getTicket(final String ticketId, final Class<? extends Ticket> clazz) {
    Assert.notNull(clazz, "clazz cannot be null");

    final Ticket ticket = this.getTicket(ticketId);

    if (ticket == null) {
        return null;
    }

    if (!clazz.isAssignableFrom(ticket.getClass())) {
        throw new ClassCastException("Ticket [" + ticket.getId()
            + " is of type " + ticket.getClass()
            + " when we were expecting " + clazz);
    }

    return (T) ticket;
}

DefaultTicketRegistry使用内存中的Map存储Ticket信息,其getTicket方法的代码如下:

public Ticket getTicket(final String ticketId) {
    if (ticketId == null) {
        return null;
    }

    logger.debug("Attempting to retrieve ticket [{}]", ticketId);
    final Ticket ticket = this.cache.get(ticketId);

    if (ticket != null) {
        logger.debug("Ticket [{}] found in registry.", ticketId);
    }

    return ticket;
}

3.3 校验ST

3.3.1 ST是否过期

ServiceTicketImplisExpired方法继承自父类AbstractTicketAbstractTicket实现了isExpired的算法模板,代码如下:

public final boolean isExpired() {
    final TicketGrantingTicket tgt = getGrantingTicket();
    return this.expirationPolicy.isExpired(this)
            || (tgt != null && tgt.isExpired())
            || isExpiredInternal();
}

public final TicketGrantingTicket getGrantingTicket() {
    return this.ticketGrantingTicket;
}

protected boolean isExpiredInternal() {
    return false;
}

this.expirationPolicy属性由DefaultServiceTicketFactory创建ServiceTicketImpl时传入,代码如下:

final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket(
        ticketId,
        service,
        this.serviceTicketExpirationPolicy,
        credentialsProvided,
        this.onlyTrackMostRecentSession);

@Resource(name="serviceTicketExpirationPolicy")
protected ExpirationPolicy serviceTicketExpirationPolicy;

serviceTicketExpirationPolicy可由用户自行配置,默认配置如下:

<!-- deployerConfigContext.xml -->
<alias name="ticketGrantingTicketExpirationPolicy" alias="grantingTicketExpirationPolicy" />
<alias name="multiTimeUseOrTimeoutExpirationPolicy" alias="serviceTicketExpirationPolicy" />

multiTimeUseOrTimeoutExpirationPolicy对应的是MultiTimeUseOrTimeoutExpirationPolicy,其isExpired方法代码如下:

// 默认超时时间为10s
@Value("#{${st.timeToKillInSeconds:10}*1000L}")
private final long timeToKillInMilliSeconds;

// 默认为1
@Value("${st.numberOfUses:1}")
private final int numberOfUses;

public boolean isExpired(final TicketState ticketState) {
    if (ticketState == null) {
        LOGGER.debug("Ticket state is null for {}", this.getClass().getSimpleName());
        return true;
    }

    // 校验ST的使用数
    final long countUses = ticketState.getCountOfUses();
    if (countUses >= this.numberOfUses) {
        LOGGER.debug("Ticket usage count {} is greater than or equal to {}", countUses, this.numberOfUses);
        return true;
    }

    final long systemTime = System.currentTimeMillis();
    final long lastTimeUsed = ticketState.getLastTimeUsed();
    final long difference = systemTime - lastTimeUsed;

    // 校验ST的超时时间
    if (difference >= this.timeToKillInMilliSeconds) {
        LOGGER.debug("Ticket has expired because the difference between current time [{}] "
            + "and ticket time [{}] is greater than or equal to [{}]", systemTime, lastTimeUsed,
            this.timeToKillInMilliSeconds);
        return true;
    }
    return false;
}

st.timeToKillInSecondsst.numberOfUses可在cas.properties文件中配置;

3.3.2 ST是否支持当前Service

代码如下:

public boolean isValidFor(final Service serviceToValidate) {
    // 更新ST的访问记录
    updateState();
    // 
    return serviceToValidate.matches(this.service);
}


// AbstractWebApplicationService.java

public boolean matches(final Service service) {
    try {
        final String thisUrl = URLDecoder.decode(this.id, "UTF-8");
        final String serviceUrl = URLDecoder.decode(service.getId(), "UTF-8");


        logger.trace("Decoded urls and comparing [{}] with [{}]", thisUrl, serviceUrl);
        return thisUrl.equalsIgnoreCase(serviceUrl);
    } catch (final Exception e) {
        logger.error(e.getMessage(), e);
    }
    return false;
}

3.4 创建认证结果

ST校验通过后,根据ST找到对应的TGT,从而找到对应的鉴权结果,然后将创建认证结果;

4 校验认证结果

代码如下:

private boolean validateAssertion(final HttpServletRequest request, final String serviceTicketId, final Assertion assertion) {
    final ValidationSpecification validationSpecification = this.getCommandClass();
    final ServletRequestDataBinder binder = new ServletRequestDataBinder(validationSpecification, "validationSpecification");
    initBinder(request, binder);
    binder.bind(request);

    // 是否满足特定校验要求
    if (!validationSpecification.isSatisfiedBy(assertion)) {
        logger.debug("Service ticket [{}] does not satisfy validation specification.", serviceTicketId);
        return false;
    }
    return true;
}

private ValidationSpecification getCommandClass() {
    try {
        return (ValidationSpecification) this.validationSpecificationClass.newInstance();
    } catch (final Exception e) {
        throw new RuntimeException(e);
    }
}

private Class<?> validationSpecificationClass = Cas20ProtocolValidationSpecification.class;

默认使用的是Cas20ProtocolValidationSpecification,其isSatisfiedBy方法继承自父类AbstractCasProtocolValidationSpecificationAbstractCasProtocolValidationSpecification实现了isSatisfiedBy的算法模板,并提供算法细节isSatisfiedByInternal由子类实现,代码如下:

public final boolean isSatisfiedBy(final Assertion assertion) {
    return isSatisfiedByInternal(assertion)
        && (!this.renew || assertion.isFromNewLogin());
}

protected abstract boolean isSatisfiedByInternal(Assertion assertion);

Cas20ProtocolValidationSpecification实现了算法细节isSatisfiedByInternal,代码如下:

protected boolean isSatisfiedByInternal(final Assertion assertion) {
    return true;
}

5 获取proxyIou

本例不涉及;

6 创建视图

代码如下:

private ModelAndView generateSuccessView(final Assertion assertion, final String proxyIou,
                                         final WebApplicationService service,
                                         final TicketGrantingTicket proxyGrantingTicket) {

    final ModelAndView modelAndView = getModelAndView(true, service);

    modelAndView.addObject(CasViewConstants.MODEL_ATTRIBUTE_NAME_ASSERTION, assertion);
    modelAndView.addObject(CasViewConstants.MODEL_ATTRIBUTE_NAME_SERVICE, service);
    modelAndView.addObject(CasViewConstants.MODEL_ATTRIBUTE_NAME_PROXY_GRANTING_TICKET_IOU, proxyIou);
    if (proxyGrantingTicket != null) {
        modelAndView.addObject(CasViewConstants.MODEL_ATTRIBUTE_NAME_PROXY_GRANTING_TICKET, proxyGrantingTicket.getId());
    }
    final Map<String, ?> augmentedModelObjects = augmentSuccessViewModelObjects(assertion);
    if (augmentedModelObjects != null) {
        modelAndView.addAllObjects(augmentedModelObjects);
    }
    return modelAndView;
}

private ModelAndView getModelAndView(final boolean isSuccess, final WebApplicationService service) {
    if (service != null){
        if (service.getFormat() == ValidationResponseType.JSON) {
            return new ModelAndView(DEFAULT_SERVICE_VIEW_NAME_JSON);
        }
    }
    return new ModelAndView(isSuccess ? this.successView : this.failureView);
}

this.successViewthis.failureViewV3ServiceValidateController重写,代码如下:

public void setFailureView(@Value("cas3ServiceFailureView") final String failureView) {
    super.setFailureView(failureView);
}

public void setSuccessView(@Value("cas3ServiceSuccessView") final String successView) {
    super.setSuccessView(successView);
}

6.1 成功视图

cas3ServiceSuccessView的配置如下:

@Component("cas3ServiceSuccessView")
public static class Success extends Cas30ResponseView {
    /**
     * Instantiates a new Success.
     * @param view the view
     */
    @Autowired
    public Success(@Qualifier("cas3JstlSuccessView")
                   final AbstractUrlBasedView view) {
        super(view);
        super.setSuccessResponse(true);
    }
}

cas3JstlSuccessView的配置如下:

<!-- protocolViewsConfiguration.xml -->
<bean id="cas3JstlSuccessView" class="org.springframework.web.servlet.view.JstlView"
      c:url="/WEB-INF/view/jsp/protocol/3.0/casServiceValidationSuccess.jsp" />

casServiceValidationSuccess.jsp文件的内容如下:

<%@ page session="false" contentType="application/xml; charset=UTF-8" %>
<%@ page import="java.util.*, java.util.Map.Entry" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationSuccess>
        <cas:user>${fn:escapeXml(principal.id)}</cas:user>
        <c:if test="${not empty pgtIou}">
            <cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
        </c:if>
        <c:if test="${fn:length(chainedAuthentications) > 0}">
            <cas:proxies>
                <c:forEach var="proxy" items="${chainedAuthentications}" varStatus="loopStatus" begin="0"
                           end="${fn:length(chainedAuthentications)}" step="1">
                    <cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
                </c:forEach>
            </cas:proxies>
        </c:if>

        <c:if test="${fn:length(attributes) > 0}">
            <cas:attributes>
                <c:forEach var="attr"
                           items="${attributes}"
                           varStatus="loopStatus" begin="0"
                           end="${fn:length(attributes)}"
                           step="1">

                    <c:forEach var="attrval" items="${attr.value}">
                        <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attrval)}</cas:${fn:escapeXml(attr.key)}>
                    </c:forEach>
                </c:forEach>
            </cas:attributes>
        </c:if>

    </cas:authenticationSuccess>
</cas:serviceResponse>

本例中,服务端只会将principal.id信息返回给客户端;

6.2 失败视图

cas3ServiceFailureView的配置如下:

<!-- protocolViewsConfiguration.xml -->
<bean id="cas3ServiceFailureView" class="org.springframework.web.servlet.view.JstlView"
      c:url="/WEB-INF/view/jsp/protocol/3.0/casServiceValidationFailure.jsp" />

casServiceValidationFailure.jsp文件的内容如下:

<%@ page session="false" contentType="application/xml; charset=UTF-8" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
    <cas:authenticationFailure code='${code}'>
            ${fn:escapeXml(description)}
    </cas:authenticationFailure>
</cas:serviceResponse>

服务端将错误码和错误信息返回给客户端;

至此,服务端校验Ticket流程结束。