CAS process analysis client user access request (carrying Ticket)

brief introduction

The user's request to access the client will first go through the filter chain configured by the client. The common filters are as follows:

  1. CAS Single Sign Out Filter-SingleSignOutFilter
    1. Realize single sign out and put it in the first position;
  2. CAS Validation Filter-Cas30ProxyReceivingTicketValidationFilter
    1. Responsible for verifying the Ticket
    2. The server address needs to be specified: casServerUrlPrefix
    3. Client address needs to be specified: serverName
  3. CAS Authentication Filter-AuthenticationFilter
    1. Responsible for user authentication
    2. The server login address needs to be specified: casServerLoginUrl
    3. Client address needs to be specified: serverName
  4. CAS HttpServletRequest Wrapper Filter-HttpServletRequestWrapperFilter
    1. Is responsible for wrapping HttpServletRequest so that the login name of the login user can be obtained through the getRemoteUser() method of HttpServletRequest

The user's access request will be intercepted and processed by the above configured filter in turn;

SingleSignOutFilter

Firstly, it is processed by SingleSignOutFilter, which realizes the single sign out function;

This is a user access request, not a login request. The filter will be released directly. The core code is as follows:

private static final SingleSignOutHandler HANDLER = new SingleSignOutHandler();

public void init(final FilterConfig filterConfig) throws ServletException {
    super.init(filterConfig);
    if (!isIgnoreInitConfiguration()) {
        // set a property
        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 initialization
    HANDLER.init();
    // Set handler initialization completion ID
    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>
     */
    // Verify that the initialization of the handler is complete
    if (!this.handlerInitialized.getAndSet(true)) {
        HANDLER.init();
    }

    // Select whether to continue the filter chain according to the handler's processing
    if (HANDLER.process(request, response)) {
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

First, analyze the operation of setting properties. The getString method is used when setting properties. This method inherits from the parent class AbstractConfigurationFilter. The code is as follows:

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

getString uses this Configurationstrategy, continue to see the assignment of this attribute. The code is as follows:

private static final String CONFIGURATION_STRATEGY_KEY = "configurationStrategy";

public void init(FilterConfig filterConfig) throws ServletException {
    // Get the configuration information of the configurationStrategy from the configuration
    final String configurationStrategyName = filterConfig.getServletContext().getInitParameter(CONFIGURATION_STRATEGY_KEY);
    // Obtain the configurationstrategy instance according to the configuration information of configurationstrategy
    this.configurationStrategy = ReflectUtils.newInstance(ConfigurationStrategyName.resolveToConfigurationStrategy(configurationStrategyName));
    // perform an initialization operation
    this.configurationStrategy.init(filterConfig, getClass());
}


ConfigurationStrategyName.java

DEFAULT(LegacyConfigurationStrategyImpl.class),

public static Class<? extends ConfigurationStrategy> resolveToConfigurationStrategy(final String value) {
    if (CommonUtils.isBlank(value)) {
        // If it is empty, the default value legacyconfigurationstrategyimpl. Is returned 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;
}

If configurationStrategy is not configured in the configuration file, LegacyConfigurationStrategyImpl instance will be created by default. This type is the wrapper of WebXmlConfigurationStrategyImpl and JNDI configurationstrategyimpl. WebXmlConfigurationStrategyImpl is preferred. The core code is as follows:

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 is preferred
    final String value1 = this.webXmlConfigurationStrategy.get(key);

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

    return this.jndiConfigurationStrategy.get(key);
}

Then, the implementation of SingleSignOutHandler is analyzed. Its core code is as follows:

public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
    if (isTokenRequest(request)) {
        // If the request carries a token
        // The Ticket information carried in this user access request will be returned here
        logger.trace("Received a token request");
        // Record session
        recordSession(request);
        // Return true to continue the filter chain
        return true;
    } else if (isBackChannelLogoutRequest(request)) {
        // If post channel logout request
        // This user access request is not a login request and is not satisfied
        logger.trace("Received a back channel logout request");
        // Destroy session
        destroySession(request);
        // Return false to terminate the filter chain
        return false;
    } else if (isFrontChannelLogoutRequest(request)) {
        // If front channel logout request
        // This user access request is not a login request and is not satisfied
        logger.trace("Received a front channel logout request");
        // Destroy session
        destroySession(request);
        // redirection url to the CAS server
        final String redirectionUrl = computeRedirectionToServer(request);
        if (redirectionUrl != null) {
            // Redirect to CAS Server
            CommonUtils.sendRedirect(response, redirectionUrl);
        }
        // Return false to terminate the filter chain
        return false;
    } else {
        logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
        // It returns true by default and continues to execute the filter chain
        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 {
        // Remove cached session information
        this.sessionMappingStorage.removeBySessionById(session.getId());
    } catch (final Exception e) {
        // ignore if the session is already marked as invalid.  Nothing we can do!
    }
    // Cache the current token and 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 {
                // Invalid session
                session.invalidate();
            } catch (final IllegalStateException e) {
                logger.debug("Error invalidating session.", e);
            }
            // Logout
            this.logoutStrategy.logout(request);
        }
    }
}

Cas30ProxyReceivingTicketValidationFilter

According to the filter configuration order, the next step is cas30proxyreceiving ticketvalidationfilter, which is responsible for the verification of tickets. The server address (casServerUrlPrefix) and client address (serverName) need to be specified;

This user access request carries Ticket information. The filter will verify the Ticket information. The core code is as follows:

public Cas30ProxyReceivingTicketValidationFilter() {
    // The supporting protocol is CAS3
    super(Protocol.CAS3);
    // The ST validator uses Cas30ServiceTicketValidator
    this.defaultServiceTicketValidatorClass = Cas30ServiceTicketValidator.class;
    // The PT validator uses Cas30ProxyTicketValidator
    this.defaultProxyTicketValidatorClass = Cas30ProxyTicketValidator.class;
}

Cas30ProxyReceivingTicketValidationFilter inherits from Cas20ProxyReceivingTicketValidationFilter and overrides this protocol,this.defaultServiceTicketValidatorClass and this The implementation of defaultproxyticketvalidatorclass is the same as other processes, and then analyze cas20proxyreceiving ticketvalidationfilter;

The doFilter method of Cas20ProxyReceivingTicketValidationFilter inherits from the parent class AbstractTicketValidationFilter. AbstractTicketValidationFilter implements the algorithm template of doFilter method and provides algorithm details. preFilter, onSuccessfulValidation and onFailedValidation can be rewritten by subclasses to add additional processing logic. The core code is as follows:

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

    // Pretreatment before filtration
    if (!preFilter(servletRequest, servletResponse, filterChain)) {
        return;
    }

    final HttpServletRequest request = (HttpServletRequest) servletRequest;
    final HttpServletResponse response = (HttpServletResponse) servletResponse;
    // Get Ticket information from the request
    final String ticket = retrieveTicketFromRequest(request);

    // Does Ticket information exist
    // This user access request carries Ticket information
    if (CommonUtils.isNotBlank(ticket)) {
        logger.debug("Attempting to validate ticket: {}", ticket);

        try {
            // If there is Ticket information, it needs to be verified
            // Verify the Ticket information carried in this user access request
            final Assertion assertion = this.ticketValidator.validate(ticket,
                    constructServiceUrl(request, response));

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

            // If no exception is thrown, the verification is successful
            // Put the verification results into the request to facilitate the next acquisition
            request.setAttribute(CONST_CAS_ASSERTION, assertion);

            if (this.useSession) {
                // In the case of using session, the verification result is stored in the session attribute of the method
                request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
            }
            // Process when verification is successful
            onSuccessfulValidation(request, response, assertion);

            if (this.redirectAfterValidation) {
                // The default value is true. Redirection is required after verification
                // After resetting, the Assertion information will exist in the request, so you can use the AuthenticationFilter
                // However, the Ticket information will not be carried, so you can directly use cas30proxyreceiving Ticket validation filter
                logger.debug("Redirecting after successful ticket validation.");
                response.sendRedirect(constructServiceUrl(request, response));
                return;
            }
        } catch (final TicketValidationException e) {
            logger.debug(e.getMessage(), e);

            // Process when verification fails
            onFailedValidation(request, response);

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

            // Return httpservletresponse SC_ Forward response
            response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());

            return;
        }
    }

    // Execute filter chain
    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) {
    // Get "ticket" information from the request
    return CommonUtils.safeGetParameter(request, this.protocol.getArtifactParameterName());
}

For this The redirectaftervalidation attribute needs to be specified. This value determines whether to redirect after the Ticket verification is successful,

  1. If redirection is performed, the new request will not carry the Ticket information, and the new request does not need to be verified again; However, since AuthenticationFilter obtains authentication results from Session, to redirect, it must support the use of Session, that is, this Usesession is true
  2. If redirection is not performed, the original request will also carry the Ticket information, and the Ticket information obtained by the AuthenticationFilter will be released;

So this The code related to the assignment of redirectaftervalidation property is as follows:

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) {
        // If Session is not used, redirection to the client after Ticket verification is not supported
        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 rewrites preFilter to preprocess requests in proxy mode. The code is as follows:

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();

    // Is it a proxy request
    // This user access request is not a proxy request
    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. This. Is used in dofilter Ticketvalidator is used to verify Ticket information. Cas30ServiceTicketValidator is used by default in Cas30ProxyReceivingTicketValidationFilter. Its validate method inherits from the parent class AbstractUrlBasedTicketValidator. AbstractUrlBasedTicketValidator implements the algorithm template of the validate method and provides algorithm details. retrieveResponseFromServer parseResponseFromServer is implemented by subclasses, and the code is as follows:

public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
    // Build verification request URL
    final String validationUrl = constructValidationUrl(ticket, service);
    logger.debug("Constructing validation url: {}", validationUrl);

    try {
        // Send the verification request and get the server response
        logger.debug("Retrieving response from server.");
        final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);

        // If the returned result is empty, the verification fails
        if (serverResponse == null) {
            throw new TicketValidationException("The CAS server returned no response.");
        }

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

        // Parsing server response
        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.");
    // Put Ticket information
    urlParameters.put("ticket", ticket);
    // Put in Service information
    urlParameters.put("service", serviceUrl);

    // If renew is included
    if (this.renew) {
        urlParameters.put("renew", "true");
    }

    logger.debug("Calling template URL attribute map.");
    // Populate other attributes
    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;

    // Construct 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 implements the algorithm template of retrieveResponseFromServer for algorithm details. The code is as follows:

protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
    // With 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);
        }

        // Get response
        final StringBuilder builder = new StringBuilder(255);
        int byteRead;
        while ((byteRead = in.read()) != -1) {
            builder.append((char) byteRead);
        }

        // Return response
        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 implements the algorithm template of the algorithm detail parseResponseFromServer, and provides the algorithm detail customParseResponse for subclass rewriting to add additional processing logic. The code is as follows:

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

    if (CommonUtils.isNotBlank(error)) {
        // There is an error message
        throw new TicketValidationException(error);
    }

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

    // Get PGT information
    final String proxyGrantingTicket;
    if (CommonUtils.isBlank(proxyGrantingTicketIou) || this.proxyGrantingTicketStorage == null) {
        proxyGrantingTicket = null;
    } else {
        proxyGrantingTicket = this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou);
    }

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

    // Encapsulate Assertion information
    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));
    }

    // Support subclass customization and parsing Assertion information
    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

Next is the authentication filter, which is responsible for user authentication. You need to specify the server login address (casServerLoginUrl) and the client address (serverName);

The user access request does not carry Ticket information or Assertion information, so it will be redirected to the server for authentication by the filter. The core code is as follows:

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;

    // This user access request does not involve
    if (isRequestUrlExcluded(request)) {
        logger.debug("Request is ignored.");
        // If the request is excluded, the filter chain is executed directly
        filterChain.doFilter(request, response);
        return;
    }
    
    // Trying to get Assertion information from Session
    final HttpSession session = request.getSession(false);
    final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;

    // This user access request is processed by Cas30ProxyReceivingTicketValidationFilter
    // If it is redirected, the Assertion information will be carried
    // If it is not redirected, Session may not be used, so the Assertion information cannot be obtained
    if (assertion != null) {
        // If Assertion exists, the filter chain is directly executed without authentication
        filterChain.doFilter(request, response);
        return;
    }

    // Construct this client URL
    final String serviceUrl = constructServiceUrl(request, response);
    // Get Ticket information from request
    final String ticket = retrieveTicketFromRequest(request);
    final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);

    // This user access request is processed by Cas30ProxyReceivingTicketValidationFilter
    // If it is not redirected, there will be Ticket information
    if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
        // If there is Ticket information, authentication is not required
        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);

    // Redirect to the server for login authentication
    // This user access request is over
    logger.debug("redirecting to \"{}\"", urlToRedirectTo);
    this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}

HttpServletRequestWrapperFilter

The next step is HttpServletRequestWrapperFilter, which is responsible for packaging HttpServletRequest, so that the login name of the login user can be obtained through the getRemoteUser() method of HttpServletRequest. The core code is as follows:

public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
        final FilterChain filterChain) throws IOException, ServletException {
    // Obtain the Principal information from the request or session
    final AttributePrincipal principal = retrievePrincipalFromSessionOrRequest(servletRequest);

    // Wrap ServletRequest and add Principal information
    filterChain.doFilter(new CasHttpServletRequestWrapper((HttpServletRequest) servletRequest, principal),
            servletResponse);
}


/**
 * Inner class
 * It inherits from HttpServletRequestWrapper and encapsulates the Principal information
 */
final class CasHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final AttributePrincipal principal;

    CasHttpServletRequestWrapper(final HttpServletRequest request, final AttributePrincipal principal) {
        super(request);
        this.principal = principal;
    }

    public Principal getUserPrincipal() {
        return this.principal;
    }

    public String getRemoteUser() {
        return principal != null ? this.principal.getName() : null;
    }

    public boolean isUserInRole(final String role) {
        if (CommonUtils.isBlank(role)) {
            logger.debug("No valid role provided.  Returning false.");
            return false;
        }

        if (this.principal == null) {
            logger.debug("No Principal in Request.  Returning false.");
            return false;
        }

        if (CommonUtils.isBlank(roleAttribute)) {
            logger.debug("No Role Attribute Configured. Returning false.");
            return false;
        }

        final Object value = this.principal.getAttributes().get(roleAttribute);

        if (value instanceof Collection<?>) {
            for (final Object o : (Collection<?>) value) {
                if (rolesEqual(role, o)) {
                    logger.debug("User [{}] is in role [{}]: true", getRemoteUser(), role);
                    return true;
                }
            }
        }

        final boolean isMember = rolesEqual(role, value);
        logger.debug("User [{}] is in role [{}]: {}", getRemoteUser(), role, isMember);
        return isMember;
    }

    /**
     * Determines whether the given role is equal to the candidate
     * role attribute taking into account case sensitivity.
     *
     * @param given  Role under consideration.
     * @param candidate Role that the current user possesses.
     *
     * @return True if roles are equal, false otherwise.
     */
    private boolean rolesEqual(final String given, final Object candidate) {
        return ignoreCase ? given.equalsIgnoreCase(candidate.toString()) : given.equals(candidate);
    }
}

So far, the processing flow of the user access request carrying the Ticket is completed.

Reference articles

CAS single sign on

Keywords: cas sso

Added by !jazz on Thu, 23 Dec 2021 21:47:58 +0200