CAS流程简析 客户端处理未携带Ticket访问请求

相关阅读

简介

用户访问客户端的请求,会先经过客户端配置的过滤器链,常用的过滤器如下:

  1. CAS Single Sign Out Filter——SingleSignOutFilter
    1. 实现单点登出,放在首个位置;
  2. CAS Validation Filter——Cas30ProxyReceivingTicketValidationFilter
    1. 负责对Ticket的校验
    2. 需要指定服务端地址:casServerUrlPrefix
    3. 需要指定客户端地址:serverName
  3. CAS Authentication Filter——AuthenticationFilter
    1. 负责用户的鉴权
    2. 需要指定服务端登录地址:casServerLoginUrl
    3. 需要指定客户端地址:serverName
  4. CAS HttpServletRequest Wrapper Filter——HttpServletRequestWrapperFilter
    1. 负责包装HttpServletRequest,从而可通过HttpServletRequestgetRemoteUser()方法获取登录用户的登录名
      用户的访问请求会依次经过以上配置的过滤器的拦截和处理;

SingleSignOutFilter

首先经过SingleSignOutFilter处理,该过滤器实现单点登出功能;
本次是用户访问请求,不是登出请求,该过滤器会直接放行,核心代码如下:

private static final SingleSignOutHandler HANDLER = new SingleSignOutHandler();

public void init(final FilterConfig filterConfig) throws ServletException {
    super.init(filterConfig);
    if (!isIgnoreInitConfiguration()) {
        // 设置属性
        setArtifactParameterName(getString(ConfigurationKeys.ARTIFACT_PARAMETER_NAME));
        setLogoutParameterName(getString(ConfigurationKeys.LOGOUT_PARAMETER_NAME));
        setFrontLogoutParameterName(getString(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME));
        setRelayStateParameterName(getString(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME));
        setCasServerUrlPrefix(getString(ConfigurationKeys.CAS_SERVER_URL_PREFIX));
        HANDLER.setArtifactParameterOverPost(getBoolean(ConfigurationKeys.ARTIFACT_PARAMETER_OVER_POST));
        HANDLER.setEagerlyCreateSessions(getBoolean(ConfigurationKeys.EAGERLY_CREATE_SESSIONS));
    }
    // handler初始化
    HANDLER.init();
    // 设置handler初始化完成标识
    handlerInitialized.set(true);
}

public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
        final FilterChain filterChain) throws IOException, ServletException {
    final HttpServletRequest request = (HttpServletRequest) servletRequest;
    final HttpServletResponse response = (HttpServletResponse) servletResponse;

    /**
     * <p>Workaround for now for the fact that Spring Security will fail since it doesn't call {@link #init(javax.servlet.FilterConfig)}.</p>
     * <p>Ultimately we need to allow deployers to actually inject their fully-initialized {@link org.jasig.cas.client.session.SingleSignOutHandler}.</p>
     */
    // 校验handler是否初始化完成
    if (!this.handlerInitialized.getAndSet(true)) {
        HANDLER.init();
    }

    // 根据handler的处理选择是否继续过滤器链
    if (HANDLER.process(request, response)) {
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

首先分析设置属性的操作,设置属性时用到了getString方法,该方法继承自父类AbstractConfigurationFilter,代码如下:

protected final String getString(final ConfigurationKey<String> configurationKey) {
    return this.configurationStrategy.getString(configurationKey);
}

getString使用了this.configurationStrategy,继续看下该属性的赋值,代码如下:

private static final String CONFIGURATION_STRATEGY_KEY = "configurationStrategy";

public void init(FilterConfig filterConfig) throws ServletException {
    // 从配置中获取configurationStrategy的配置信息
    final String configurationStrategyName = filterConfig.getServletContext().getInitParameter(CONFIGURATION_STRATEGY_KEY);
    // 根据configurationStrategy的配置信息获取ConfigurationStrategy实例
    this.configurationStrategy = ReflectUtils.newInstance(ConfigurationStrategyName.resolveToConfigurationStrategy(configurationStrategyName));
    // 执行初始化操作
    this.configurationStrategy.init(filterConfig, getClass());
}


// ConfigurationStrategyName.java

DEFAULT(LegacyConfigurationStrategyImpl.class),

public static Class<? extends ConfigurationStrategy> resolveToConfigurationStrategy(final String value) {
    if (CommonUtils.isBlank(value)) {
        // 若为空,返回默认值LegacyConfigurationStrategyImpl.class
        return DEFAULT.configurationStrategyClass;
    }

    for (final ConfigurationStrategyName csn : values()) {
        if (csn.name().equalsIgnoreCase(value)) {
            return csn.configurationStrategyClass;
        }
    }

    try {
        final Class<?> clazz = Class.forName(value);

        if (ConfigurationStrategy.class.isAssignableFrom(clazz)) {
            return (Class<? extends ConfigurationStrategy>) clazz;
        }
    }   catch (final ClassNotFoundException e) {
        LOGGER.error("Unable to locate strategy {} by name or class name.  Using default strategy instead.", value, e);
    }

    return DEFAULT.configurationStrategyClass;
}

若配置文件中没有配置configurationStrategy,则默认创建LegacyConfigurationStrategyImpl实例,该类型是WebXmlConfigurationStrategyImplJndiConfigurationStrategyImpl的包装,优先使用WebXmlConfigurationStrategyImpl,核心代码如下:

private final WebXmlConfigurationStrategyImpl webXmlConfigurationStrategy = new WebXmlConfigurationStrategyImpl();
private final JndiConfigurationStrategyImpl jndiConfigurationStrategy = new JndiConfigurationStrategyImpl();

public void init(FilterConfig filterConfig, Class<? extends Filter> filterClazz) {
    this.webXmlConfigurationStrategy.init(filterConfig, filterClazz);
    this.jndiConfigurationStrategy.init(filterConfig, filterClazz);
}

protected String get(final ConfigurationKey key) {
    // 优先使用WebXmlConfigurationStrategyImpl
    final String value1 = this.webXmlConfigurationStrategy.get(key);

    if (CommonUtils.isNotBlank(value1)) {
        return value1;
    }

    return this.jndiConfigurationStrategy.get(key);
}

接着分析SingleSignOutHandler的实现,其核心代码如下:

public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
    if (isTokenRequest(request)) {
        // 若是携带token的请求
        // 本次用户访问请求未携带Ticket信息,不满足
        logger.trace("Received a token request");
        // 记录session
        recordSession(request);
        // 返回true,继续执行过滤器链
        return true;
    } else if (isBackChannelLogoutRequest(request)) {
        // 若是后置通道登出请求
        // 本次用户访问请求不是登出请求,不满足
        logger.trace("Received a back channel logout request");
        // 销毁session
        destroySession(request);
        // 返回false,终止执行过滤器链
        return false;
    } else if (isFrontChannelLogoutRequest(request)) {
        // 若是前置通道登出请求
        // 本次用户访问请求不是登出请求,不满足
        logger.trace("Received a front channel logout request");
        // 销毁session
        destroySession(request);
        // redirection url to the CAS server
        final String redirectionUrl = computeRedirectionToServer(request);
        if (redirectionUrl != null) {
            // 重定向到CAS Server
            CommonUtils.sendRedirect(response, redirectionUrl);
        }
        // 返回false,终止执行过滤器链
        return false;
    } else {
        logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
        // 默认返回true,继续执行过滤器链
        // 本次用户访问请求会由这里直接返回true
        return true;
    }
}

private boolean isTokenRequest(final HttpServletRequest request) {
    return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName,
            this.safeParameters));
}

private boolean isBackChannelLogoutRequest(final HttpServletRequest request) {
    return "POST".equals(request.getMethod())
            && !isMultipartRequest(request)
            && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
                    this.safeParameters));
}

private boolean isFrontChannelLogoutRequest(final HttpServletRequest request) {
    return "GET".equals(request.getMethod()) && CommonUtils.isNotBlank(this.casServerUrlPrefix)
            && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName));
}

private void recordSession(final HttpServletRequest request) {
    final HttpSession session = request.getSession(this.eagerlyCreateSessions);

    if (session == null) {
        logger.debug("No session currently exists (and none created).  Cannot record session information for single sign out.");
        return;
    }

    final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);
    logger.debug("Recording session for token {}", token);

    try {
        // 移除已缓存的session信息
        this.sessionMappingStorage.removeBySessionById(session.getId());
    } catch (final Exception e) {
        // ignore if the session is already marked as invalid.  Nothing we can do!
    }
    // 缓存当前token和session
    sessionMappingStorage.addSessionById(token, session);
}

private void destroySession(final HttpServletRequest request) {
    final String logoutMessage;
    // front channel logout -> the message needs to be base64 decoded + decompressed
    if (isFrontChannelLogoutRequest(request)) {
        logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request,
                this.frontLogoutParameterName));
    } else {
        logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
    }
    logger.trace("Logout request:\n{}", logoutMessage);

    final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
    if (CommonUtils.isNotBlank(token)) {
        final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);

        if (session != null) {
            final String sessionID = session.getId();
            logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);

            try {
                // 失效session
                session.invalidate();
            } catch (final IllegalStateException e) {
                logger.debug("Error invalidating session.", e);
            }
            // 登出
            this.logoutStrategy.logout(request);
        }
    }
}

Cas30ProxyReceivingTicketValidationFilter

根据过滤器配置顺序,接下来就是Cas30ProxyReceivingTicketValidationFilter,该过滤器负责对Ticket的校验,需要指定服务端地址casServerUrlPrefix和客户端地址serverName
本次用户访问请求未携带Ticket信息,该过滤器会直接放行,核心代码如下:

public Cas30ProxyReceivingTicketValidationFilter() {
    // 支持协议为CAS3
    super(Protocol.CAS3);
    // ST校验器使用Cas30ServiceTicketValidator
    this.defaultServiceTicketValidatorClass = Cas30ServiceTicketValidator.class;
    // PT校验器使用Cas30ProxyTicketValidator
    this.defaultProxyTicketValidatorClass = Cas30ProxyTicketValidator.class;
}

Cas30ProxyReceivingTicketValidationFilter继承自Cas20ProxyReceivingTicketValidationFilter,重写了this.protocolthis.defaultServiceTicketValidatorClassthis.defaultProxyTicketValidatorClass的实现,其它处理都一样,接着分析Cas20ProxyReceivingTicketValidationFilter
Cas20ProxyReceivingTicketValidationFilterdoFilter方法继承自父类AbstractTicketValidationFilterAbstractTicketValidationFilter实现了doFilter方法的算法模板,并提供算法细节preFilteronSuccessfulValidationonFailedValidation可供子类重写添加额外处理逻辑,核心代码如下:

public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
        final FilterChain filterChain) throws IOException, ServletException {

    // 过滤前预处理
    if (!preFilter(servletRequest, servletResponse, filterChain)) {
        return;
    }

    final HttpServletRequest request = (HttpServletRequest) servletRequest;
    final HttpServletResponse response = (HttpServletResponse) servletResponse;
    // 从请求中获取Ticket信息
    final String ticket = retrieveTicketFromRequest(request);

    // 是否存在Ticket信息
    // 本次用户访问请求未携带Ticket信息,直接跳过,执行过滤器链
    if (CommonUtils.isNotBlank(ticket)) {
        logger.debug("Attempting to validate ticket: {}", ticket);

        try {
            // 存在Ticket信息就需要校验
            final Assertion assertion = this.ticketValidator.validate(ticket,
                    constructServiceUrl(request, response));

            logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());

            // 未抛出异常,则校验成功
            // 将校验结果放入request请求中,方便下次获取
            request.setAttribute(CONST_CAS_ASSERTION, assertion);

            if (this.useSession) {
                // 使用session情况下,将校验结果方法session属性中
                request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
            }
            // 校验成功时处理
            onSuccessfulValidation(request, response, assertion);

            if (this.redirectAfterValidation) {
                // 默认为true,校验后需要重定向
                logger.debug("Redirecting after successful ticket validation.");
                response.sendRedirect(constructServiceUrl(request, response));
                return;
            }
        } catch (final TicketValidationException e) {
            logger.debug(e.getMessage(), e);

            // 校验失败时处理
            onFailedValidation(request, response);

            if (this.exceptionOnValidationFailure) {
                throw new ServletException(e);
            }

            // 返回HttpServletResponse.SC_FORBIDDEN响应
            response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());

            return;
        }
    }

    // 执行过滤器链
    filterChain.doFilter(request, response);
}

protected boolean preFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
        final FilterChain filterChain) throws IOException, ServletException {
    return true;
}

protected void onSuccessfulValidation(final HttpServletRequest request, final HttpServletResponse response,
        final Assertion assertion) {
    // nothing to do here.
}

protected void onFailedValidation(final HttpServletRequest request, final HttpServletResponse response) {
    // nothing to do here.
}

protected String retrieveTicketFromRequest(final HttpServletRequest request) {
    // 从请求中获取"ticket"信息
    return CommonUtils.safeGetParameter(request, this.protocol.getArtifactParameterName());
}

对于this.redirectAfterValidation属性需要说明下,该值决定了Ticket校验成功后是否需要重定向:

  1. 如果进行重定向,那么新请求就不会携带Ticket信息,新请求也就不需要再进行Ticket校验处理;但由于AuthenticationFilter是从Session中获取鉴权结果,所以要进行重定向,就必须要支持使用Session,即this.useSession为true;
  2. 如果不进行重定向,那么原请求还会携带Ticket信息,AuthenticationFilter获取到Ticket信息就会放行;

所以this.redirectAfterValidation属性赋值的相关代码如下:

private boolean redirectAfterValidation = true;

protected void initInternal(final FilterConfig filterConfig) throws ServletException {
    setExceptionOnValidationFailure(getBoolean(ConfigurationKeys.EXCEPTION_ON_VALIDATION_FAILURE));
    setRedirectAfterValidation(getBoolean(ConfigurationKeys.REDIRECT_AFTER_VALIDATION));
    setUseSession(getBoolean(ConfigurationKeys.USE_SESSION));

    if (!this.useSession && this.redirectAfterValidation) {
        // 如果不使用Session,那么就不支持Ticket校验后重定向到客户端
        logger.warn("redirectAfterValidation parameter may not be true when useSession parameter is false. Resetting it to false in order to prevent infinite redirects.");
        setRedirectAfterValidation(false);
    }

    setTicketValidator(getTicketValidator(filterConfig));
    super.initInternal(filterConfig);
}

Cas20ProxyReceivingTicketValidationFilter重写了preFilter,主要针对代理模式下的请求做预处理,代码如下:

protected final boolean preFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
                                  final FilterChain filterChain) throws IOException, ServletException {
    final HttpServletRequest request = (HttpServletRequest) servletRequest;
    final HttpServletResponse response = (HttpServletResponse) servletResponse;
    final String requestUri = request.getRequestURI();

    // 是否是代理请求
    // 本次用户访问请求不是代理请求
    if (CommonUtils.isEmpty(this.proxyReceptorUrl) || !requestUri.endsWith(this.proxyReceptorUrl)) {
        return true;
    }

    try {
        CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage);
    } catch (final RuntimeException e) {
        logger.error(e.getMessage(), e);
        throw e;
    }

    return false;
}

AbstractTicketValidationFilter.doFilter中使用了this.ticketValidator来校验Ticket信息,Cas30ProxyReceivingTicketValidationFilter中默认使用的是Cas30ServiceTicketValidator,其validate方法继承自父类AbstractUrlBasedTicketValidatorAbstractUrlBasedTicketValidator实现了validate方法的算法模板,提供算法细节retrieveResponseFromServerparseResponseFromServer由子类实现,代码如下:

public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
    // 构建校验请求URL
    final String validationUrl = constructValidationUrl(ticket, service);
    logger.debug("Constructing validation url: {}", validationUrl);

    try {
        // 发送校验请求并获取服务端响应
        logger.debug("Retrieving response from server.");
        final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);

        // 返回结果为空则校验失败
        if (serverResponse == null) {
            throw new TicketValidationException("The CAS server returned no response.");
        }

        logger.debug("Server response: {}", serverResponse);

        // 解析服务端响应
        return parseResponseFromServer(serverResponse);
    } catch (final MalformedURLException e) {
        throw new TicketValidationException(e);
    }
}

protected abstract String retrieveResponseFromServer(URL validationUrl, String ticket);

protected abstract Assertion parseResponseFromServer(final String response) throws TicketValidationException;

protected final String constructValidationUrl(final String ticket, final String serviceUrl) {
    final Map<String, String> urlParameters = new HashMap<String, String>();

    logger.debug("Placing URL parameters in map.");
    // 放入Ticket信息
    urlParameters.put("ticket", ticket);
    // 放入Service信息
    urlParameters.put("service", serviceUrl);

    // 如果包含renew
    if (this.renew) {
        urlParameters.put("renew", "true");
    }

    logger.debug("Calling template URL attribute map.");
    // 填充其他属性
    populateUrlAttributeMap(urlParameters);
    logger.debug("Loading custom parameters from configuration.");
    if (this.customParameters != null) {
        urlParameters.putAll(this.customParameters);
    }

    final String suffix = getUrlSuffix();
    final StringBuilder buffer = new StringBuilder(urlParameters.size() * 10 + this.casServerUrlPrefix.length()
            + suffix.length() + 1);

    int i = 0;

    // 构造URL
    buffer.append(this.casServerUrlPrefix);
    if (!this.casServerUrlPrefix.endsWith("/")) {
        buffer.append("/");
    }
    buffer.append(suffix);

    for (Map.Entry<String, String> entry : urlParameters.entrySet()) {
        final String key = entry.getKey();
        final String value = entry.getValue();

        if (value != null) {
            buffer.append(i++ == 0 ? "?" : "&");
            buffer.append(key);
            buffer.append("=");
            final String encodedValue = encodeUrl(value);
            buffer.append(encodedValue);
        }
    }

    return buffer.toString();
}

AbstractCasProtocolUrlBasedTicketValidator实现了算法细节的retrieveResponseFromServer的算法模板,代码如下:

protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
    // 借助CommonUtils实现
    return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
}


// CommonUtils.java
public static String getResponseFromServer(final URL constructedUrl, final HttpURLConnectionFactory factory,
        final String encoding) {

    HttpURLConnection conn = null;
    InputStreamReader in = null;
    try {
        conn = factory.buildHttpURLConnection(constructedUrl.openConnection());

        if (CommonUtils.isEmpty(encoding)) {
            in = new InputStreamReader(conn.getInputStream());
        } else {
            in = new InputStreamReader(conn.getInputStream(), encoding);
        }

        // 获取响应
        final StringBuilder builder = new StringBuilder(255);
        int byteRead;
        while ((byteRead = in.read()) != -1) {
            builder.append((char) byteRead);
        }

        // 返回响应
        return builder.toString();
    } catch (final Exception e) {
        LOGGER.error(e.getMessage(), e);
        throw new RuntimeException(e);
    } finally {
        closeQuietly(in);
        if (conn != null) {
            conn.disconnect();
        }
    }
}

Cas20ServiceTicketValidator实现了算法细节parseResponseFromServer的算法模板,并提供算法细节customParseResponse供子类重写添加额外处理逻辑,代码如下:

protected final Assertion parseResponseFromServer(final String response) throws TicketValidationException {
    final String error = XmlUtils.getTextForElement(response, "authenticationFailure");

    if (CommonUtils.isNotBlank(error)) {
        // 存在错误信息
        throw new TicketValidationException(error);
    }

    final String principal = XmlUtils.getTextForElement(response, "user");
    final String proxyGrantingTicketIou = XmlUtils.getTextForElement(response, "proxyGrantingTicket");

    // 获取PGT信息
    final String proxyGrantingTicket;
    if (CommonUtils.isBlank(proxyGrantingTicketIou) || this.proxyGrantingTicketStorage == null) {
        proxyGrantingTicket = null;
    } else {
        proxyGrantingTicket = this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou);
    }

    // 校验principal信息
    if (CommonUtils.isEmpty(principal)) {
        throw new TicketValidationException("No principal was found in the response from the CAS server.");
    }

    // 封装Assertion信息
    final Assertion assertion;
    final Map<String, Object> attributes = extractCustomAttributes(response);
    if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
        final AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes,
                proxyGrantingTicket, this.proxyRetriever);
        assertion = new AssertionImpl(attributePrincipal);
    } else {
        assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
    }

    // 支持子类定制化解析Assertion信息
    customParseResponse(response, assertion);

    return assertion;
}

protected Map<String, Object> extractCustomAttributes(final String xml) {
    final SAXParserFactory spf = SAXParserFactory.newInstance();
    spf.setNamespaceAware(true);
    spf.setValidating(false);
    try {
        final SAXParser saxParser = spf.newSAXParser();
        final XMLReader xmlReader = saxParser.getXMLReader();
        final CustomAttributeHandler handler = new CustomAttributeHandler();
        xmlReader.setContentHandler(handler);
        xmlReader.parse(new InputSource(new StringReader(xml)));
        return handler.getAttributes();
    } catch (final Exception e) {
        logger.error(e.getMessage(), e);
        return Collections.emptyMap();
    }
}

protected void customParseResponse(final String response, final Assertion assertion)
        throws TicketValidationException {
    // nothing to do
}

AuthenticationFilter

接下来就是AuthenticationFilter,该过滤器负责用户的鉴权,需要指定服务端登录地址casServerLoginUrl和客户端地址serverName
本次用户访问请求未携带Ticket信息,也不存在Assertion信息,所以会被该过滤器重定向到服务端进行鉴权处理,核心代码如下:

public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
        final FilterChain filterChain) throws IOException, ServletException {
    
    final HttpServletRequest request = (HttpServletRequest) servletRequest;
    final HttpServletResponse response = (HttpServletResponse) servletResponse;

    // 本次用户访问请求不涉及
    if (isRequestUrlExcluded(request)) {
        logger.debug("Request is ignored.");
        // 若排除该请求,则直接执行过滤器链
        filterChain.doFilter(request, response);
        return;
    }
    
    // 尝试获取Assertion信息
    // 本次用户访问请求不存在Assertion信息
    final HttpSession session = request.getSession(false);
    final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;

    if (assertion != null) {
        // 若Assertion存在,则无需鉴权,直接执行过滤器链
        filterChain.doFilter(request, response);
        return;
    }

    // 构造本客户端URL
    final String serviceUrl = constructServiceUrl(request, response);
    // 从request请求中获取Ticket信息
    final String ticket = retrieveTicketFromRequest(request);
    final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);

    // 本次用户访问请求未携带Ticket信息
    if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
        // 若存在Ticket信息则无需鉴权
        filterChain.doFilter(request, response);
        return;
    }

    final String modifiedServiceUrl;

    logger.debug("no ticket and no assertion found");
    if (this.gateway) {
        logger.debug("setting gateway attribute in session");
        modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
    } else {
        modifiedServiceUrl = serviceUrl;
    }

    logger.debug("Constructed service url: {}", modifiedServiceUrl);

    final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
            getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);

    // 重定向到服务端进行登录鉴权
    // 本次用户访问请求到此结束
    logger.debug("redirecting to \"{}\"", urlToRedirectTo);
    this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}

至此,本次未携带Ticket的用户访问请求处理流程结束。