Processing flow configuration of login request
/WEB-INF/web. The configuration of the welcome page in XML is as follows:
web.xml <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>
index. The contents of the JSP are as follows:
<%@ page language="java" session="false" %> <% final String queryString = request.getQueryString(); final String url = request.getContextPath() + "/login" + (queryString != null ? '?' + queryString : ""); response.sendRedirect(response.encodeURL(url));%>
Redirect access requests to[ http://ip:port/cas/login?queryString ];
At / WEB-INF / Web The servlet for intercepting and processing login requests is defined in the XML. The configuration is as follows:
web.xml <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 will be introduced into / WEB-INF / CAS servlet XML, which defines the mapping logic of the login request. The related configurations are as follows:
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] is configured in / WEB-INF / spring configuration / webflowcontext.xml. The configuration file is imported from / WEB-INF/web.xml, as follows:
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>
Configuration file / WEB-INF / spring configuration / webflowcontext The configuration of [loginFlowRegistry] in XML is as follows:
webflowContext.xml <webflow:flow-registry id="loginFlowRegistry" flow-builder-services="builder" base-path="/WEB-INF/webflow"> <webflow:flow-location-pattern value="/login/*-webflow.xml"/> </webflow:flow-registry>
The process configuration file path is defined: / WEB-INF / webflow / login / * - webflow XML, i.e. / WEB-INF / webflow / login / login webflow XML, which defines the processing flow of login request;
The processing flow of login request is started
According to / WEB-INF / CAS servlet XML configuration:
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"/>
It can be seen from [p:supportedFlowId="login"] that the request[ http://ip:port/cas/login?queryString ]It will be processed by [loginHandlerAdapter], and the defined [loginFlowExecutor] attribute will be referenced to [loginFlowRegistry], that is, the processing flow configured in / WEB-INF/webflow/login/login-webflow.xml file;
First analyze the processing of SelectiveFlowHandlerAdapter. Its core logic is inherited from the parent class FlowHandlerAdapter. The code is as follows:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { FlowHandler flowHandler = (FlowHandler)handler; this.checkAndPrepare(request, response, false); // According to CAS servlet XML configuration < bean id = "loginflowurlhandler" class = "org. Jasig. CAS. Web. Flow. Casdefaultflowurlhandler" / > // flowUrlHandler is CasDefaultFlowUrlHandler // The implementation of getFlowExecutionKey(request) is: request getParameter("execution") String flowExecutionKey = this.flowUrlHandler.getFlowExecutionKey(request); // For the first request, the "execution" property of the request is 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 is "login" String flowId = this.getFlowId(flowHandler, request); MutableAttributeMap<Object> input = this.getInputMap(flowHandler, request); ServletExternalContext context = this.createServletExternalContext(request, response); // Start login process processing 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; }
Where this flowExecutor. Launchexecution is flowexecutorimpl The code of launchexecution is as follows:
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); // Load the process definition according to [loginFlowRegistry] FlowDefinition flowDefinition = this.definitionLocator.getFlowDefinition(flowId); // Create a process according to [loginFlowExecutionFactory] FlowExecution flowExecution = this.executionFactory.createFlowExecution(flowDefinition); // Open process 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; }
Detailed description of the processing flow of login request
The processing flow of login request is defined in / WEB-INF / webflow / login / login webflow XML file;
First, execute the < on Start > action. The configuration is as follows:
login-webflow.xml <on-start> <evaluate expression="initialFlowSetupAction"/> </on-start>
[initialflowsetupaction] corresponds to initialflowsetupaction. Its parent class AbstractAction defines the algorithm template to be executed. The code is as follows:
public final Event execute(RequestContext context) throws Exception { Event result = this.doPreExecute(context); if (result == null) { // Start processing flow 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 implements the algorithm details doExecute. The code is as follows:
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 + '/' : "/"; // Set cookie path 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()); } // Save TGT information in cookie WebUtils.putTicketGrantingTicketInScopes(context, this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request)); // Save the WARNING information in the cookie WebUtils.putWarningCookie(context, Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request))); // Get the Service information carried in the request final Service service = WebUtils.getService(this.argumentExtractors, context); if (service != null) { logger.debug("Placing service in context scope: [{}]", service.getId()); // Find the registered Service information according to the Service information carried in the request final RegisteredService registeredService = this.servicesManager.findServiceBy(service); // The default is DefaultRegisteredServiceAccessStrategy, which supports access if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) { logger.debug("Placing registered service [{}] with id [{}] in context scope", registeredService.getServiceId(), registeredService.getId()); // Save registered Service information 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()); // Save redirect URL information WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl()); } } } else if (!this.enableFlowOnAbsentServiceRequest) { // Empty Service requests are not supported 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")); } // Save Service information WebUtils.putService(context, service); return result("success"); }
The processing of the Service information carried in the login request is as follows:
- Obtain the Service object from the request context;
- If the Service object exists, then
- Search the Service information registered in the system according to the Service object;
- If the registered Service information exists and access is allowed, then
- Save the registered Service information into the request context;
- If there is an unauthorized URL in the registered Service information, save the unauthorized URL to the request context;
- If the Service object does not exist, judge whether it supports processing requests that do not carry Service,
- If it is not supported, throw an exception;
- Save the Service object to the request context;
- Return success event;
1. Get Service object from request context
The code is as follows:
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) { // Traverse the ArgumentExtractor collection for (final ArgumentExtractor argumentExtractor : argumentExtractors) { final WebApplicationService service = argumentExtractor .extractService(request); if (service != null) { return service; } } return null; }
services-context.xml <util:list id="argumentExtractors"> <ref bean="defaultArgumentExtractor"/> </util:list>
[defaultargumentextractor] corresponds to defaultargumentextractor. Its extractService inherits from the parent class AbstractArgumentExtractor. AbstractArgumentExtractor implements the algorithm template of extractService, provides algorithm details, extractServiceInternal is implemented by subclasses, and also provides serviceFactoryList for subclass implementation. Its code is as follows:
public final WebApplicationService extractService(final HttpServletRequest request) { // Extract Service information from request 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; }
services-context.xml <util:list id="serviceFactoryList" value-type="org.jasig.cas.authentication.principal.ServiceFactory"> <ref bean="webApplicationServiceFactory" /> </util:list>
The DefaultArgumentExtractor implements the algorithm details extractServiceInternal. The code is as follows:
public WebApplicationService extractServiceInternal(final HttpServletRequest request) { for (final ServiceFactory<? extends WebApplicationService> factory : getServiceFactories()) { final WebApplicationService service = factory.createService(request); if (service != null) { // If the creation is successful, it will be returned directly logger.debug("Created {} based on {}", service, factory); return service; } } logger.debug("No service could be extracted based on the given request"); return null; }
[webapplicationservicefactory] corresponds to webapplicationservicefactory, and its createService method code is as follows:
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 is preferred serviceToUse = targetService; } else if (StringUtils.isNotBlank(service)) { // Second, use the service in the request parameter serviceToUse = service; } else { // Finally, use the service in the request attribute serviceToUse = serviceAttribute; } // Verify service information if (StringUtils.isBlank(serviceToUse)) { return null; } // Remove jsession information final String id = AbstractServiceFactory.cleanupUrl(serviceToUse); // Get the ticket information in the request parameter and take it as the artifact ID of the Service final String artifactId = request.getParameter(CasProtocolConstants.PARAMETER_TICKET); final Response.ResponseType type = HttpMethod.POST.name().equalsIgnoreCase(method) ? Response.ResponseType.POST : Response.ResponseType.REDIRECT; // Create SimpleWebApplicationServiceImpl final SimpleWebApplicationServiceImpl webApplicationService = new SimpleWebApplicationServiceImpl(id, serviceToUse, artifactId, new WebApplicationServiceResponseBuilder(type)); try { if (StringUtils.isNotBlank(format)) { // If format information exists in the request parameters, set this property 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; }
The Service created by WebApplicationServiceFactory is SimpleWebApplicationServiceImpl, which supports single sign out;
public final class SimpleWebApplicationServiceImpl extends AbstractWebApplicationService public abstract class AbstractWebApplicationService implements SingleLogoutService public interface SingleLogoutService extends WebApplicationService public interface WebApplicationService extends Service
2. Find the Service information registered in the system according to the Service object
The code is as follows:
final RegisteredService registeredService = this.servicesManager.findServiceBy(service); private ServicesManager servicesManager; @Autowired public void setServicesManager(@Qualifier("servicesManager") final ServicesManager servicesManager) { this.servicesManager = servicesManager; }
[servicesManager] corresponds to DefaultServicesManagerImpl. The code of findServiceBy method is as follows:
public RegisteredService findServiceBy(final Service service) { final Collection<RegisteredService> c = convertToTreeSet(); // Traverse registered services for (final RegisteredService r : c) { if (r.matches(service)) { // If it matches, the registered Service will be returned 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<>(); // With this Service registrydao loads the registration information 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(); }
There are many implementations of ServiceRegistryDao interface. You can configure the implementation of this interface in the configuration file according to the actual needs; Take InMemoryServiceRegistryDaoImpl as an example to analyze the implementation of the load method. The code is as follows:
public List<RegisteredService> load() { return this.registeredServices; } @PostConstruct public void afterPropertiesSet() { final String[] aliases = this.applicationContext.getAutowireCapableBeanFactory().getAliases("inMemoryServiceRegistryDao"); // If "inMemoryServiceRegistryDao" is configured if (aliases.length > 0) { LOGGER.debug("{} is used as the active service registry dao", this.getClass().getSimpleName()); try { // Locate the configuration of "inMemoryRegisteredServices" from the IOC container 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()); } } }
The configuration information of [inMemoryRegisteredServices] is as follows:
<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>
The implementation class of the commonly used RegisteredService interface is RegexRegisteredService, which supports regular expressions. The implementation code of its match method is as follows:
public boolean matches(final Service service) { if (this.servicePattern == null) { this.servicePattern = RegexUtils.createPattern(this.serviceId); } // Match according to the regular rules of serviceId return service != null && this.servicePattern != null && this.servicePattern.matcher(service.getId()).matches(); }
Because there is no start state, the first action state is executed according to the process configuration. The configuration is as follows:
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] corresponds to TicketGrantingTicketCheckAction, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:
protected Event doExecute(final RequestContext requestContext) throws Exception { // Get TGT information from context final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext); if (!StringUtils.hasText(tgtId)) { // Not if it does not exist_ Exists event return new Event(this, NOT_EXISTS); } // Invalid default TGT String eventId = INVALID; try { // Get ticket information final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class); if (ticket != null && !ticket.isExpired()) { // TGT effective eventId = VALID; } } catch (final AbstractTicketException e) { logger.trace("Could not retrieve ticket id {} from registry.", e); } return new Event(this, eventId); }
During the login request, there is no TGT information in the cookie, so go to [notExists], i.e. [gatewayRequestCheck], and its configuration is as follows:
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>
According to the expression, the login request without gateway information will go through [serviceAuthorizationCheck], and its configuration is as follows:
login-webflow.xml <action-state id="serviceAuthorizationCheck"> <evaluate expression="serviceAuthorizationCheck"/> <transition to="initializeLogin"/> </action-state>
[serviceauthorizationcheck] corresponds to serviceauthorizationcheck, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:
protected Event doExecute(final RequestContext context) throws Exception { // Get Service information from context final Service service = WebUtils.getService(context); //No service == plain /login request. Return success indicating transition to the login form if (service == null) { // There is no Service returned directly return success(); } if (this.servicesManager.getAllServices().isEmpty()) { // No Service is configured 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); } // Get matching registered services final RegisteredService registeredService = this.servicesManager.findServiceBy(service); if (registeredService == null) { // If there is no matching registered Service, the authorization verification fails 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()) { // Without permission, authorization verification fails 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); // Set redirect URL WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context, registeredService.getAccessStrategy().getUnauthorizedRedirectUrl()); throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg); } return success(); }
The Service information carried in the login request can match the registered Service information and support access, so go to [initializeLogin], and its configuration is as follows:
login-webflow.xml <action-state id="initializeLogin"> <evaluate expression="'success'"/> <transition on="success" to="viewLoginForm"/> </action-state>
According to the value of the expression, go directly to [viewLoginForm], and its configuration is as follows:
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> 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"/>
According to the configuration of [urlBasedViewResolver] and registeredservicethemebasedviewresolver.this_location_pattern (% s/%s/ui /)
// viewName is casLoginView final String defaultThemePrefix = String.format(THEME_LOCATION_PATTERN, getPrefix(), "default"); final String defaultViewUrl = defaultThemePrefix + viewName + getSuffix();
It can be seen that the corresponding page of [casLoginView] is / WEB-INF/view/jsp/default/ui/casLoginView.jsp. The main contents of this page are as follows:
<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. The parameters submitted by the JSP page are bound to the UsernamePasswordCredential object. After the page is submitted, go to realSubmit. Its configuration is as follows:
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] corresponds to authenticationviaformaction. The code of its submit method is as follows:
public final Event submit(final RequestContext context, final Credential credential, final MessageContext messageContext) { if (isRequestAskingForServiceTicket(context)) { // If ST is requested, create ST return grantServiceTicket(context, credential); } // Create TGT return createTicketGrantingTicket(context, credential, messageContext); } protected boolean isRequestAskingForServiceTicket(final RequestContext context) { // Get TGT information from context final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); // Get Service information from context final Service service = WebUtils.getService(context); // If the "renew" attribute of the request exists and both TGT information and Service information exist, the request is considered to be used to obtain ST // The login request does not meet the determination condition 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()); // Login parameters submitted by packaging page final AuthenticationTransaction transaction = AuthenticationTransaction.wrap(credential); // Authentication login parameters this.authenticationSystemSupport.getAuthenticationTransactionManager().handle(transaction, builder); final AuthenticationContext authenticationContext = builder.build(service); // After the verification is passed, a TGT is created final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationContext); // Put TGT into context 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); // Verification failed return newEvent(AUTHENTICATION_FAILURE, e); } catch (final Exception e) { logger.debug(e.getMessage(), e); // Process handling exception return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e); } }
The core logic is as follows:
- Packaging login parameters;
- Authentication login parameters;
- Create TGT;
1. Packaging login parameters
In fact, the valid Credential is packaged as an AuthenticationTransaction. The code is as follows:
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) { // Filter invalid values it.remove(); } } return set; } return Collections.emptySet(); }
2. Authentication login parameters
Implement authentication with PolicyBasedAuthenticationManager and store the authentication results in AuthenticationContextBuilder. The code is as follows:
public AuthenticationTransactionManager handle(final AuthenticationTransaction authenticationTransaction, final AuthenticationContextBuilder authenticationContext) throws AuthenticationException { if (!authenticationTransaction.getCredentials().isEmpty()) { // Complete authentication with PolicyBasedAuthenticationManager final Authentication authentication = this.authenticationManager.authenticate(authenticationTransaction); LOGGER.debug("Successful authentication; Collecting authentication result [{}]", authentication); // Collect authentication results authenticationContext.collect(authentication); } LOGGER.debug("Transaction ignored since there are no credentials to authenticate"); return this; }
PolicyBasedAuthenticationManager implements the AuthenticationManager interface. The core code is as follows:
public Authentication authenticate(final AuthenticationTransaction transaction) throws AuthenticationException { // authentication final AuthenticationBuilder builder = authenticateInternal(transaction.getCredentials()); final Authentication authentication = builder.build(); final Principal principal = authentication.getPrincipal(); if (principal instanceof NullPrincipal) { // Authentication failed throw new UnresolvedPrincipalException(authentication); } // Add the authentication method attribute, that is, AuthenticationHandler addAuthenticationMethodAttribute(builder, authentication); logger.info("Authenticated {} with credentials {}.", principal, transaction.getCredentials()); logger.debug("Attribute map for {}: {}", principal.getId(), principal.getAttributes()); // Fill in authentication metadata attribute populateAuthenticationMetadataAttributes(builder, transaction.getCredentials()); // Building Authentication return builder.build(); } protected AuthenticationBuilder authenticateInternal(final Collection<Credential> credentials) throws AuthenticationException { // Initial constructor final AuthenticationBuilder builder = new DefaultAuthenticationBuilder(NullPrincipal.getInstance()); // Populate Credential data for (final Credential c : credentials) { builder.addCredential(new BasicCredentialMetaData(c)); } boolean found; // Traverse Credential for (final Credential credential : credentials) { found = false; // Traverse the configured AuthenticationHandler and PrincipalResolver MAP for (final Map.Entry<AuthenticationHandler, PrincipalResolver> entry : this.handlerResolverMap.entrySet()) { final AuthenticationHandler handler = entry.getKey(); // Does the current AuthenticationHandler support processing the current Credential if (handler.supports(credential)) { // There is an AuthenticationHandler that handles the current Credential found = true; try { // Authenticate and parse Principal authenticateAndResolvePrincipal(builder, credential, entry.getValue(), handler); // Determine whether the exit conditions are met 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()); } } // Verify the generated authentication context // If there is no successfully authenticated HandlerResult or the conditions of AuthenticationPolicy are not met, the corresponding exception will be thrown evaluateProducedAuthenticationContext(builder); return builder; }
The core logic is as follows:
- Traverse the Credential set and authenticate each Credential;
- For the current Credential, traverse the configured handlerResolverMap to find the matching AuthenticationHandler;
- Authenticate the current Credential according to the matched AuthenticationHandler and PrincipalResolver;
- If authentication is successful, determine whether to end authentication according to the conditions of AuthenticationPolicy;
- After that, the authentication result is returned directly;
- If not, continue to the next round of AuthenticationHandler and PrincipalResolver, and then continue to the next round of Credential;
- If authentication fails, AuthenticationBuilder records the failure information;
- If authentication is successful, determine whether to end authentication according to the conditions of AuthenticationPolicy;
- After traversing the Credential set, verify the AuthenticationBuilder;
2.1} traversing the Credential set
In the login request processing process, there is only one Credential in the Credential set;
2.2} traverse the configured handlerResolverMap
The configuration information of handlerResolverMap is as follows:
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] corresponds to acceptusersauthenticationhandler. Its matching logic is inherited from the parent class AbstractUsernamePasswordAuthenticationHandler. The code is as follows:
public boolean supports(final Credential credential) { // Whether it is of UsernamePasswordCredential type, return credential instanceof UsernamePasswordCredential; }
[proxyAuthenticationHandler] corresponds to HttpBasedServiceCredentialsAuthenticationHandler. The matching logic is as follows:
public boolean supports(final Credential credential) { return credential instanceof HttpBasedServiceCredential; }
login-webflow.xml <var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>
According to the configuration defined by [credential], accepturussauthenticationhandler will process the credential submitted by this login request, and accepturussauthenticationhandler will not process the credential submitted by this login request;
2.3. AuthenticationHandler and PrincipalResolver authenticate the current Credential
The authentication code is as follows:
private void authenticateAndResolvePrincipal(final AuthenticationBuilder builder, final Credential credential, final PrincipalResolver resolver, final AuthenticationHandler handler) throws GeneralSecurityException, PreventedException { Principal principal; // Authentication is completed by AuthenticationHandler final HandlerResult result = handler.authenticate(credential); // Authentication succeeded, and the AuthenticationHandler information is recorded builder.addSuccess(handler.getName(), result); logger.info("{} successfully authenticated {}", handler.getName(), credential); if (resolver == null) { // If the parser is empty, the Principal is taken from HandlerResult principal = result.getPrincipal(); logger.debug( "No resolver configured for {}. Falling back to handler principal {}", handler.getName(), principal); } else { // Parsing 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()); // If the resolution fails, the Principal is obtained from HandlerResult principal = result.getPrincipal(); } } // Must avoid null principal since AuthenticationBuilder/ImmutableAuthentication // require principal to be non-null if (principal != null) { // Set Principal builder.setPrincipal(principal); } logger.debug("Final principal resolved for this authentication event is {}", principal); }
Firstly, the AuthenticationHandler, AcceptUsersAuthenticationHandler, authenticates the current Credential. Its parent class AbstractPreAndPostProcessingAuthenticationHandler implements the algorithm template of authenticate and provides the default implementation of preAuthenticate and postAuthenticate algorithm details. It can be rewritten by subclasses. It provides doAuthentication algorithm details, which are implemented by subclasses, The code is as follows:
public final HandlerResult authenticate(final Credential credential) throws GeneralSecurityException, PreventedException { // Authentication preprocessing if (!preAuthenticate(credential)) { throw new FailedLoginException(); } // Post authentication processing 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; // Provide an interface to create HandlerResult for subclass implementation protected final HandlerResult createHandlerResult(final Credential credential, final Principal principal, final List<MessageDescriptor> warnings) { return new DefaultHandlerResult(this, new BasicCredentialMetaData(credential), principal, warnings); }
The parent class AbstractUsernamePasswordAuthenticationHandler of AcceptUsersAuthenticationHandlerde implements the algorithm template of abstract doAuthentication algorithm details, and provides authenticateUsernamePasswordInternal algorithm details. It is implemented by subclasses, and its code is as follows:
protected final HandlerResult doAuthentication(final Credential credential) throws GeneralSecurityException, PreventedException { final UsernamePasswordCredential userPass = (UsernamePasswordCredential) credential; if (userPass.getUsername() == null) { // Throw an exception if there is no user name throw new AccountNotFoundException("Username is null."); } // Policy mode for user name conversion is supported // Nopprincipalnametransformer is used by default without conversion final String transformedUsername= this.principalNameTransformer.transform(userPass.getUsername()); if (transformedUsername == null) { throw new AccountNotFoundException("Transformed username is null."); } // Update to converted user name userPass.setUsername(transformedUsername); // Authentication converted data return authenticateUsernamePasswordInternal(userPass); } protected abstract HandlerResult authenticateUsernamePasswordInternal(UsernamePasswordCredential transformedCredential) throws GeneralSecurityException, PreventedException;
AcceptUsersAuthenticationHandlerde implements the details of the authenticateUsernamePasswordInternal algorithm. The code is as follows:
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) { // If the configured user information (configured by cas.properties) exists, the currently cached user information does not exist, and the configured user information is used // Configuration information format: 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()) { // If the current user information does not exist, login is not supported 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) { // If the password corresponding to the user does not exist, the account does not exist logger.debug("{} was not found in the map.", username); throw new AccountNotFoundException(username + " not found in backing map."); } // Encrypt submitted password information // PlainTextPasswordEncoder is used by default, i.e. no encryption final String encodedPassword = this.getPasswordEncoder().encode(credential.getPassword()); // Compare password information if (!cachedPassword.equals(encodedPassword)) { throw new FailedLoginException(); } // If the password verification passes, a HandlerResult is created // By default, DefaultPrincipalFactory is used to create SimplePrincipal, and its attribute is empty MAP return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null); } public final void setUsers(@NotNull final Map<String, String> users) { this.users = Collections.unmodifiableMap(users); }
User information is transferred in two ways:
1. Inject through setUsers, with high priority;
deployerConfigContext.xml <bean id="acceptUsersAuthenticationHandler" class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler"> <property name="users"> <map> <entry key="casuser" value="Mellon"/> </map> </property> </bean>
2. Through CAS Properties configuration, low priority;
cas.properties accept.authn.users=casuser::Mellon
If the AuthenticationHandler fails to authenticate, an exception is thrown to end; If the AuthenticationHandler is authenticated successfully, the PrincipalResolver is used to parse the Principal information of the current Credential. The code is as follows:
protected Principal resolvePrincipal( final String handlerName, final PrincipalResolver resolver, final Credential credential) { // Does the current PrincipalResolver support resolving credentials if (resolver.supports(credential)) { try { // Resolve the Principal information of the current Credential 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; }
deployerConfigContext.xml <alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />
According to the configuration of [primaryPrincipalResolver], [persondirectoryprincipalresolver] corresponds to persondirectoryprincipalresolver, which will process the Credential submitted by this login request. The code is as follows:
/** * Does this PrincipalResolver support handling Credential */ public boolean supports(final Credential credential) { // It is supported as long as the ID of the Credential exists return credential != null && credential.getId() != null; } /** * Resolve Credential */ public Principal resolve(final Credential credential) { logger.debug("Attempting to resolve a principal..."); final String principalId = extractPrincipalId(credential); // Verify principalId if (principalId == null) { logger.debug("Got null for extracted principal ID; returning null."); return null; } logger.debug("Creating SimplePrincipal for [{}]", principalId); // Gets the property collection corresponding to the principalId final Map<String, List<Object>> attributes = retrievePersonAttributes(principalId, credential); // Verification attribute set if (attributes == null || attributes.isEmpty()) { logger.debug("Principal id [{}] did not specify any attributes", principalId); // Return null directly if (!this.returnNullIfNoAttributes) { logger.debug("Returning the principal with id [{}] without any attributes", principalId); // Return null property 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()); // When converting the attribute collection, the principalId parameter may be updated if this. Is configured Principalattributename attribute final Pair<String, Map<String, Object>> pair = convertPersonAttributesToPrincipal(principalId, attributes); // Create a Principal based on the principalId and attribute set 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) { // Query user attributes from IPersonAttributeDao final IPersonAttributes personAttributes = this.attributeRepository.getPerson(principalId); final Map<String, List<Object>> attributes; // Verify the queried user 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<>(); // Default 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)) { // If this is configured Principalattributename attribute, the first attribute corresponding to the attribute value is used as the principalId if (values.isEmpty()) { logger.debug("{} is empty, using {} for principal", this.principalAttributeName, extractedPrincipalId); } else { // Update principalId principalId = values.get(0).toString(); logger.debug( "Found principal attribute value {}; removing {} from attribute map.", extractedPrincipalId, this.principalAttributeName); } } else { // Save attribute values convertedAttributes.put(key, values.size() == 1 ? values.get(0) : values); } } return new Pair<>(principalId, convertedAttributes); }
Firstly, we analyze the method to obtain the user attribute set: retrievePersonAttributes, which uses this The attributerepository attribute is completed. The core code of the assignment logic of the attribute is as follows:
// Stubbersonattributedao is used by default protected IPersonAttributeDao attributeRepository = new StubPersonAttributeDao(new HashMap<String, List<Object>>()); // It can be customized through Setter injection public final void setAttributeRepository(@Qualifier("attributeRepository") final IPersonAttributeDao attributeRepository) { this.attributeRepository = attributeRepository; }
[attributeRepository] is defined in the deployerConfigContext.xml configuration file. The configuration is as follows:
deployerConfigContext.xml <bean id="attributeRepository" class="org.jasig.services.persondir.support.NamedStubPersonAttributeDao" p:backingMap-ref="attrRepoBackingMap" /> <util:map id="attrRepoBackingMap"> <entry key="uid" value="uid" /> <entry key="eduPersonAffiliation" value="eduPersonAffiliation" /> <entry key="groupMembership" value="groupMembership" /> <entry> <key><value>memberOf</value></key> <list> <value>faculty</value> <value>staff</value> <value>org</value> </list> </entry> </util:map>
The getPerson implementation of NamedStubPersonAttributeDao inherits from the parent class StubPersonAttributeDao. The code is as follows:
public IPersonAttributes getPerson(String uid) { if (uid == null) { throw new IllegalArgumentException("Illegal to invoke getPerson(String) with a null argument."); } else { // Directly return this Backingperson, i.e. "attrRepoBackingMap" in the configuration file return this.backingPerson; } }
Then analyze the logic of convertPersonAttributesToPrincipal, which will be based on the configured this Principalattributename attribute (if configured), take the first value from the attribute set corresponding to the attribute value to update the principalId information. The core code of the assignment logic of this attribute is as follows:
// Optional attribute values protected String principalAttributeName; // It can be customized through Setter injection public void setPrincipalAttributeName(@Value("${cas.principal.resolver.persondir.principal.attribute:}") final String attribute) { this.principalAttributeName = attribute; }
This parameter can be configured in CAS In the properties file, the default value is:
cas.properties #cas.principal.resolver.persondir.principal.attribute=cn
2.3. 1. Authentication is successful. Determine whether to end authentication according to the conditions of AuthenticationPolicy
After successful authentication, it will go through this Authenticationpolicy determines whether the current authentication process can be ended. The core code of the assignment logic of this attribute is as follows:
// The default is AnyAuthenticationPolicy, which is satisfied as long as there is a successfully authenticated Credential private AuthenticationPolicy authenticationPolicy = new AnyAuthenticationPolicy(); // It can be customized through Setter injection @Resource(name="authenticationPolicy") public void setAuthenticationPolicy(final AuthenticationPolicy policy) { this.authenticationPolicy = policy; }
Analyze the implementation of isSatisfiedBy of AuthenticationPolicy. The code is as follows:
// Default false private boolean tryAll; // It can be customized through Setter injection public void setTryAll(@Value("${cas.authn.policy.any.tryall:false}") final boolean tryAll) { this.tryAll = tryAll; } public boolean isSatisfiedBy(final Authentication authn) { // Whether to traverse all. The default is false. Users can use CAS Properties configuration if (this.tryAll) { return authn.getCredentials().size() == authn.getSuccesses().size() + authn.getFailures().size(); } // As long as there is a Credential with successful authentication return !authn.getSuccesses().isEmpty(); }
2.3. 2. Authentication failed. AuthenticationBuilder records the failure information
After authentication fails, AuthenticationBuilder records the failure information, then continues the next round of AuthenticationHandler and PrincipalResolver, and then continues the next round of Credential;
2.4 verifying AuthenticationBuilder
After traversing the Credential set, the validity of AuthenticationBuilder will be verified before exiting normally. If there is no Credential with successful authentication or this If the end condition of authenticationpolicy, an exception is thrown. The code is as follows:
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()); } }
After authenticateInternal authentication, the validity of the Principal of the authentication result will be verified again (NullPrincipal may be returned in the authentication process), then the information of AuthenticationHandler will be added, and then the metadata attribute will be filled through the configured this.authenticationMetaDataPopulators, and finally the authentication result will be returned;
At this point, the authentication process ends;
3. Create TGT
After the authentication is successful, this The centralauthenticationservice attribute creates TGT information. The core code of the assignment logic of this attribute is as follows:
@Autowired @Qualifier("centralAuthenticationService") private CentralAuthenticationService centralAuthenticationService;
[centralAuthenticationService] corresponds to CentralAuthenticationServiceImpl, and its TGT creation code is as follows:
public TicketGrantingTicket createTicketGrantingTicket(final AuthenticationContext context) throws AuthenticationException, AbstractTicketException { // Obtain authentication results final Authentication authentication = context.getAuthentication(); // Get TGT create factory final TicketGrantingTicketFactory factory = this.ticketFactory.get(TicketGrantingTicket.class); // Create TGT based on authentication results final TicketGrantingTicket ticketGrantingTicket = factory.create(authentication); // TGT created by cache this.ticketRegistry.addTicket(ticketGrantingTicket); // Publish TGT creation event doPublishEvent(new CasTicketGrantingTicketCreatedEvent(this, ticketGrantingTicket)); return ticketGrantingTicket; }
this. The TicketFactory property stores the TicketFactory information to be configured. The core code of the assignment logic of this property is as follows:
@Resource(name="defaultTicketFactory") protected TicketFactory ticketFactory;
[defaultticketfactory] corresponds to defaultticketfactory. The core code is:
// Save instances of various ticketfactories 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(); // Find the corresponding instance from the cached factoryMap according to class return (T) this.factoryMap.get(clazz.getCanonicalName()); }
Through this ticketFactory. Get (TicketGrantingTicket.class) can find [defaultTicketGrantingTicketFactory], namely DefaultTicketGrantingTicketFactory, and then call its create method to create TGT. The code is as follows:
public <T extends TicketGrantingTicket> T create(final Authentication authentication) { final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl( this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(TicketGrantingTicket.PREFIX), authentication, ticketGrantingTicketExpirationPolicy); return (T) ticketGrantingTicket; }
The final created TGT is TicketGrantingTicketImpl, and its id value is determined by this The ticketgrantingticketuniqueticketidgenerator attribute is generated. The core code of the assignment logic of this attribute is as follows:
@NotNull @Resource(name="ticketGrantingTicketUniqueIdGenerator") protected UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator;
[ticketgrantingticketiqueidgenerator] corresponds to TicketGrantingTicketIdGenerator. Its getNewTicketId method inherits from the parent defaultuniquetickeidgenerator. The code is as follows:
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(); }
At this point, the TGT creation process ends;
After the TGT is successfully created, it will go through the [success] process, that is, [gatewayRequestCheck], and its configuration is as follows:
login-webflow.xml <action-state id="sendTicketGrantingTicket"> <evaluate expression="sendTicketGrantingTicketAction"/> <transition to="serviceCheck"/> </action-state>
[sendticketgrantingticketaction] corresponds to sendticketgrantingticketaction, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:
protected Event doExecute(final RequestContext context) { // Get TGT from context final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); // Get TGT from cookie final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId"); if (ticketGrantingTicketId == null) { // TGT direct return does not exist in the context return success(); } if (isAuthenticatingAtPublicWorkstation(context)) { // If you authenticate through the public workbench, no cookie s will be generated 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."); // Set TGT cookie this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils .getHttpServletResponse(context), ticketGrantingTicketId); } if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) { // If the TGT in the original cookie is different from the TGT in the context, the TGT in the original cookie will be destroyed this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie); } return success(); }
Destroying TGT is implemented by CentralAuthenticationServiceImpl. The code is as follows:
public List<LogoutRequest> destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) { try { logger.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId); // Query TGT according to TGT ID final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class); logger.debug("Ticket found. Processing logout requests and then deleting the ticket..."); // Log out the single sign out service using the TGT final List<LogoutRequest> logoutRequests = logoutManager.performLogout(ticket); // Remove TGT from cache this.ticketRegistry.deleteTicket(ticketGrantingTicketId); // Release TGT destruction event 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.performLogout is implemented by logotmanagerimpl. The code is as follows:
public List<LogoutRequest> performLogout(final TicketGrantingTicket ticket) { // Gets the Service collection of the current TGT binding final Map<String, Service> services = ticket.getServices(); final List<LogoutRequest> logoutRequests = new ArrayList<>(); // if SLO is not disabled if (!this.singleLogoutCallbacksDisabled) { // Enable SLO function // 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) { // If it is a single sign out service, you need to log out 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) { // Is the service logged out if (!singleLogoutService.isLoggedOutAlready()) { // Found registered service final RegisteredService registeredService = servicesManager.findServiceBy(singleLogoutService); // Determine whether the service supports single sign out if (serviceSupportsSingleLogout(registeredService)) { // Gets the logout URL of the service final URL logoutUrl = determineLogoutUrl(registeredService, singleLogoutService); // Encapsulate logout request final DefaultLogoutRequest logoutRequest = new DefaultLogoutRequest(ticketId, singleLogoutService, logoutUrl); // Gets the logout type of the service final LogoutType type = registeredService.getLogoutType() == null ? LogoutType.BACK_CHANNEL : registeredService.getLogoutType(); // Perform corresponding operations according to the login type of the service switch (type) { case BACK_CHANNEL: // Attempt to send a logout request 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: // Other types, the reverse channel logout operation is not supported 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); // Priority is given to the login URL of the registered service 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(); // Set the logout ID of the service 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); // Send logout request return this.httpClient.sendMessageToEndPoint(msg); } catch (final Exception e) { LOGGER.error(e.getMessage(), e); } return false; }
The cookie in the login request will not carry TGT information. It will directly return success() and go to [serviceCheck] for processing. Its configuration is as follows:
login-webflow.xml <decision-state id="serviceCheck"> <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess"/> </decision-state>
The cookie in the login request carries the Service information and goes to [generateServiceTicket] for processing. Its configuration is as follows:
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] corresponds to generateserviceticketaction, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:
protected Event doExecute(final RequestContext context) { // Get Service information from context final Service service = WebUtils.getService(context); // Get TGT information from context 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. */ // Obtain the corresponding authentication result according to TGT final Authentication authentication = ticketRegistrySupport.getAuthenticationFrom(ticketGrantingTicket); if (authentication == null) { // If TGT has no corresponding authentication result, it is invalid throw new InvalidTicketException(new AuthenticationException(), ticketGrantingTicket); } final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder( this.authenticationSystemSupport.getPrincipalElectionStrategy()); final AuthenticationContext authenticationContext = builder.collect(authentication).build(service); // Create ST according to TGT, Service, etc 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) { // Destroy TGT this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket); } if (isGatewayPresent(context)) { return result("gateway"); } return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e); } return error(); }
this.centralAuthenticationService is CentralAuthenticationServiceImpl, which was introduced when creating TGT. The grantServiceTicket method is introduced here. The code is as follows:
public ServiceTicket grantServiceTicket( final String ticketGrantingTicketId, final Service service, final AuthenticationContext context) throws AuthenticationException, AbstractTicketException { // Get cached TGT information according to TGT ID final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class); // Obtain registered Service information according to Service information matching final RegisteredService registeredService = this.servicesManager.findServiceBy(service); // Verify registered Service information verifyRegisteredServiceProperties(registeredService, service); // Verify whether the current authentication result is consistent with the authentication result cached in TGT final Authentication currentAuthentication = evaluatePossibilityOfMixedPrincipals(context, ticketGrantingTicket); // Verify the usage record of TGT and the access policy of registered Service if (ticketGrantingTicket.getCountOfUses() > 0 && !registeredService.getAccessStrategy().isServiceAccessAllowedForSso()) { // If TGT has bound ST and the currently registered Service does not support SSO, throw an exception logger.warn("Service [{}] is not allowed to use SSO.", service.getId()); throw new UnauthorizedSsoServiceException(); } // Verify agent Service if necessary // This example does not involve 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 // Obtain the authentication results that meet the security policy // AcceptAnyAuthenticationPolicyFactory is used by default, which is satisfied by default getAuthenticationSatisfiedByPolicy(ticketGrantingTicket.getRoot(), new ServiceContext(service, registeredService)); // Obtain the authentication result of the TGT cache and the authentication result of the cache of the ST associated with the TGT final List<Authentication> authentications = ticketGrantingTicket.getChainedAuthentications(); // Obtain the Principal of the last authentication result final Principal principal = authentications.get(authentications.size() - 1).getPrincipal(); // The default is empty final RegisteredServiceAttributeReleasePolicy releasePolicy = registeredService.getAttributeReleasePolicy(); final Map<String, Object> principalAttrs; if (releasePolicy != null) { principalAttrs = releasePolicy.getAttributes(principal); } else { principalAttrs = new HashMap<>(); } // Verify whether the principal meets the requirements of registeredserviceattributrereleasepolicy. The default principal attributes is empty, so it meets the requirements 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(); } // Get the ServiceTicketFactory instance, DefaultServiceTicketFactory final ServiceTicketFactory factory = this.ticketFactory.get(ServiceTicket.class); // Create ST according to TGT Service final ServiceTicket serviceTicket = factory.create(ticketGrantingTicket, service, currentAuthentication != null); // Cache ST this.ticketRegistry.addTicket(serviceTicket); logger.info("Granted ticket [{}] for service [{}] and principal [{}]", serviceTicket.getId(), service.getId(), principal.getId()); // Publish ST creation event doPublishEvent(new CasServiceTicketGrantedEvent(this, ticketGrantingTicket, serviceTicket)); // Return to 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(); // Verify whether the current authentication result is consistent with the authentication result cached in 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; } }; }
The create method of DefaultServiceTicketFactory is as follows:
public <T extends Ticket> T create(final TicketGrantingTicket ticketGrantingTicket, final Service service, final boolean credentialsProvided) { final String uniqueTicketIdGenKey = service.getClass().getName(); UniqueTicketIdGenerator serviceTicketUniqueTicketIdGenerator = null; // Attempt to get Service specific 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) { // If the Service does not have a specific UniqueTicketIdGenerator, the default value is used serviceTicketUniqueTicketIdGenerator = this.defaultServiceTicketIdGenerator; logger.debug("Service ticket id generator not found for [{}]. Using the default generator...", uniqueTicketIdGenKey); } // Generate ST ID final String ticketId = serviceTicketUniqueTicketIdGenerator.getNewTicketId(ServiceTicket.PREFIX); // Create ST with TGT final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket( ticketId, service, this.serviceTicketExpirationPolicy, credentialsProvided, this.onlyTrackMostRecentSession); return (T) serviceTicket; }
TGT is of TicketGrantingTicketImpl type. The code of its grantServiceTicket method is as follows:
public final synchronized ServiceTicket grantServiceTicket(final String id, final Service service, final ExpirationPolicy expirationPolicy, final boolean credentialsProvided, final boolean onlyTrackMostRecentSession) { // Create 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(); // Update service Principal information final List<Authentication> authentications = getChainedAuthentications(); service.setPrincipal(authentications.get(authentications.size()-1).getPrincipal()); // If you enable tracking of recent access sessions, you need to clear the cached service information, if any 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)) { // If the cache information of the service exists, it needs to be cleared existingServices.remove(existingService); LOGGER.trace("Removed previous tickets for service: {}", existingService); break; } } } // Cache the information of the service this.services.put(id, service); } protected final void updateState() { // Update record this.previousLastTimeUsed = this.lastTimeUsed; this.lastTimeUsed = System.currentTimeMillis(); this.countOfUses++; }
At this point, the process of creating ST ends;
After the ST is successfully created, it will go through the [success] process, that is, [warn], and its configuration is as follows:
login-webflow.xml <decision-state id="warn"> <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/> </decision-state>
According to the expression, if there is no warn information, go to [redirect], and its configuration is as follows:
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>
According to the expression, get the Response with ST ID information according to the service. The code is as follows:
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)) { // Save ST ID information 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"); }
After creating the Response, go to [postRedirectDecision], and its configuration is as follows:
login-webflow.xml <decision-state id="postRedirectDecision"> <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView"/> </decision-state>
According to the expression, if the response is of POST type, go to postView; otherwise, go to redirectView; in this case, go to redirectView. The configuration is as follows:
login-webflow.xml <end-state id="redirectView" view="externalRedirect:#{requestScope.response.url}"/>
According to the expression, redirection is realized through ExternalRedirectAction, which inherits from AbstractAction and implements the algorithm details doExecute. The code is as follows:
protected Event doExecute(RequestContext context) throws Exception { String resourceUri = (String)this.resourceUri.getValue(context); context.getExternalContext().requestExternalRedirect(resourceUri); return this.success(); }
Redirect to the Service carried in the login request and carry the newly created ST information;
At this point, the processing flow of the login request without Service is completed.