在工作中经常会对CAS进行二次改造适应不同的单点登录场景。这篇文章主要对CAS 4.1.10版本进行源码解读(主要是登录流程)。不同版本可以在github下载。
下载下来的cas-overlay-template的依赖中默认只有
<dependency> <groupId>org.jasig.cas</groupId> <artifactId>cas-server-webapp</artifactId> <version>${cas.version}</version> <type>war</type> <scope>runtime</scope> </dependency>为了跟踪相关的代码还需要添加下面的两个依赖
<dependency> <groupId>org.jasig.cas</groupId> <artifactId>cas-server-core</artifactId> <version>${cas.version}</version> </dependency> <dependency> <groupId>org.jasig.cas</groupId> <artifactId>cas-server-webapp-support</artifactId> <version>${cas.version}</version> </dependency>CAS 是使用SpringMVC+Spring WebFlow(工作流框架)控制登录,登出流程的。
一般情况下,我们在浏览器访问http://localhost:8080/cas,cas 服务端会默认访问index.jsp页面
<%@ page language="java" session="false" %> <% final String queryString = request.getQueryString(); final String url = request.getContextPath() + "/login" + (queryString != null ? "?" + queryString : ""); response.sendRedirect(response.encodeURL(url));%>从上面index.jsp页面的内容发现,它会从定向到http://localhost:8080/cas/login,该路径是由名为cas 的 servlet进行处理的
<servlet> <servlet-name>cas</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-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> </servlet> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>这里将请求交给了SpringMVC进行处理。
在上面的servlet配置中会发现,其核心的配置文件是/WEB-INF/cas-servlet.xml, /WEB-INF/cas-servlet-*.xml。SpringMVC在初始化的时候会去自动加载cas-servlet.xml或cas-servlet-*.xml配置。在WEB-INF目录下我们找到了cas-servlet.xml这个文件,里面对SpringMVC和Spring WebFlow进行了整合配置。如果想深入了解可以参考整合细节
<!--为login登录请求开启流处理--> <bean id="loginHandlerAdapter" class="org.jasig.cas.web.flow.SelectiveFlowHandlerAdapter" p:supportedFlowId="login" p:flowExecutor-ref="loginFlowExecutor" p:flowUrlHandler-ref="loginFlowUrlHandler" /> <!-- 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" /> </array> </property> </bean> <!--注册一个工作流节点,login的请求交由login-webflow.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> <!--view-factory-creator属性,该属性就定义了视图解析工厂--> <webflow:flow-builder-services id="builder" view-factory-creator="viewFactoryCreator" expression-parser="expressionParser" /> <bean id="viewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator"> <property name="viewResolvers"> <util:list> <ref bean="viewResolver"/> <ref bean="internalViewResolver"/> </util:list> </property> </bean> <!-- View Resolver --> <bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver" p:order="0"> <property name="basenames"> <util:list> <value>cas_views</value> </util:list> </property> </bean>如果对SpringMVC的请求路径是login,那么SpringMVC会交给webflow进行处理。flow-builder-services节点中有个view-factory-creator属性,该属性定义了视图解析工厂。该视图解析工厂是由视图解析器组成的。这里只定义了一个视图解析器,就是viewResolvers。该视图解析器是springFramework中的ResourceBundleViewResolver的一个实例,该类可以通过basenames属性,找到value值对应的properties属性文件,该文件中式类似ke=values类型的内容,正是该文件将jsp文件映射成视图名称。
由上面的分析知道,登录流程的配置文件是/WEB-INF/webflow/login目录下的login-webflow。
<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>定义UsernamePasswordCredential类型的变量,用于存放用户名和密码(默认),可进行扩展存放更多的属性。
<on-start> <evaluate expression="initialFlowSetupAction"/> </on-start>这是流程开始的操作,要去执行initialFlowSetupAction这个bean,它定义在cas-servlet.xml里面
<bean id="initialFlowSetupAction" class="org.jasig.cas.web.flow.InitialFlowSetupAction" p:argumentExtractors-ref="argumentExtractors" p:warnCookieGenerator-ref="warnCookieGenerator" p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator" p:servicesManager-ref="servicesManager" p:enableFlowOnAbsentServiceRequest="${create.sso.missing.service:true}" />org.jasig.cas.web.flow.InitialFlowSetupAction继承自AbstractAction,AbstractAction方法是org.springframework.webflow.action包中的类,是webflow中的基础类。该类中的doExecute方法是对应处理业务的方法。
protected Event doExecute(RequestContext context) throws Exception { HttpServletRequest request = WebUtils.getHttpServletRequest(context); String contextPath = context.getExternalContext().getContextPath(); String cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/"; if (StringUtils.isBlank(this.warnCookieGenerator.getCookiePath())) { this.logger.info("Setting path for cookies for warn cookie generator to: {} ", cookiePath); this.warnCookieGenerator.setCookiePath(cookiePath); } else { this.logger.debug("Warning cookie path is set to {} and path {}", this.warnCookieGenerator.getCookieDomain(), this.warnCookieGenerator.getCookiePath()); } if (StringUtils.isBlank(this.ticketGrantingTicketCookieGenerator.getCookiePath())) { this.logger.info("Setting path for cookies for TGC cookie generator to: {} ", cookiePath); this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath); } else { this.logger.debug("TGC cookie path is set to {} and path {}", this.ticketGrantingTicketCookieGenerator.getCookieDomain(), this.ticketGrantingTicketCookieGenerator.getCookiePath()); } //将TGT放在RequestScope作用域中 WebUtils.putTicketGrantingTicketInScopes(context, this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request)); //将warnCookieValue放在RequestScope作用域中 WebUtils.putWarningCookie(context, Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request))); //获取service参数 Service service = WebUtils.getService(this.argumentExtractors, context); if (service != null) { this.logger.debug("Placing service in context scope: [{}]", service.getId()); //查找注册的service RegisteredService registeredService = this.servicesManager.findServiceBy(service); if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) { this.logger.debug("Placing registered service [{}] with id [{}] in context scope", registeredService.getServiceId(), registeredService.getId()); WebUtils.putRegisteredService(context, registeredService); RegisteredServiceAccessStrategy accessStrategy = registeredService.getAccessStrategy(); if (accessStrategy.getUnauthorizedRedirectUrl() != null) { this.logger.debug("Placing registered service's unauthorized redirect url [{}] with id [{}] in context scope", accessStrategy.getUnauthorizedRedirectUrl(), registeredService.getServiceId()); WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl()); } } } else if (!this.enableFlowOnAbsentServiceRequest) { this.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")); } WebUtils.putService(context, service); return this.result("success"); }该方法的参数是RequestContext对象,该参数是一个流程的容器。该方法从request中获取TGT,并且构建一个临时的service对象(不同域注册的service,详情见接入系统管理)。并且,将TGT,warnCookieValue和service放在RequestContext作用域中,以便在登录流程中的state中进行判断
<action-state id="ticketGrantingTicketCheck"> <evaluate expression="ticketGrantingTicketCheckAction"/> <transition on="notExists" to="gatewayRequestCheck"/> <transition on="invalid" to="terminateSession"/> <transition on="valid" to="hasServiceCheck"/> </action-state>初始化完成后,登录流程流转到第一个state(ticketGrantingTicketExistsCheck)
<bean id="ticketGrantingTicketCheckAction" class="org.jasig.cas.web.flow.TicketGrantingTicketCheckAction" c:centralAuthenticationService-ref="centralAuthenticationService" />它会去执行ticketGrantingTicketCheck的doExecute方法检查requestContext中是否存在TGT,TGT是否有效
protected Event doExecute(RequestContext requestContext) throws Exception { String tgtId = WebUtils.getTicketGrantingTicketId(requestContext); if (!StringUtils.hasText(tgtId)) { return new Event(this, "notExists"); } else { String eventId = "invalid"; try { //验证TGT是否有效 Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class); if (ticket != null && !ticket.isExpired()) { eventId = "valid"; } } catch (TicketException var5) { this.logger.trace("Could not retrieve ticket id {} from registry.", var5); } return new Event(this, eventId); } }第一次访问应用系统http://app1.example.com,此时应用系统会跳转到CAS单点登录的服务器端http://127.0.0.1:8081/cas-server/login?service=http%3a%2f%2fapp1.example.com,此时,request的cookies中不存在CASTGC(TGT),因此RequestContext作用域中的ticketGrantingTicketId为null,登录流程流转到第二个state(gatewayRequestCheck)
<decision-state id="gatewayRequestCheck"> <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null" then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck"/> </decision-state>初始化时,把service保存在了RequestContext作用域中,但request中的参数gateway不存在,登录流程流转到第三个state(serviceAuthorizationCheck)
<!-- Do a service authorization check early without the need to login first --> <action-state id="serviceAuthorizationCheck"> <evaluate expression="serviceAuthorizationCheck"/> <transition to="generateLoginTicket"/> </action-state> <bean id="serviceAuthorizationCheck" class="org.jasig.cas.web.flow.ServiceAuthorizationCheck" c:servicesManager-ref="servicesManager" />执行它的doExecute方法
protected Event doExecute(RequestContext context) throws Exception { Service service = WebUtils.getService(context); if (service == null) { return this.success(); } else if (this.servicesManager.getAllServices().isEmpty()) { 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()); this.logger.warn(msg); throw new UnauthorizedServiceException("screen.service.empty.error.message"); } else { RegisteredService registeredService = this.servicesManager.findServiceBy(service); String msg; if (registeredService == null) { msg = String.format("Service Management: Unauthorized Service Access. Service [%s] is not found in service registry.", service.getId()); this.logger.warn(msg); throw new UnauthorizedServiceException("screen.service.error.message", msg); } else if (!registeredService.getAccessStrategy().isServiceAccessAllowed()) { msg = String.format("Service Management: Unauthorized Service Access. Service [%s] is not allowed access via the service registry.", service.getId()); this.logger.warn(msg); WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context, registeredService.getAccessStrategy().getUnauthorizedRedirectUrl()); throw new UnauthorizedServiceException("screen.service.error.message", msg); } else { return this.success(); } } }doExecute方法,要做的就是判断RequestContext作用域中是否存在service,如果service存在,查找service的注册信息。登录流程流转到第四个state(generateLoginTicket)
<action-state id="generateLoginTicket"> <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)"/> <transition on="generated" to="viewLoginForm"/> </action-state> <bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction" p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator"/>执行generate方法
public class GenerateLoginTicketAction { private static final String PREFIX = "LT"; private final Logger logger = LoggerFactory.getLogger(this.getClass()); @NotNull private UniqueTicketIdGenerator ticketIdGenerator; public GenerateLoginTicketAction() { } public final String generate(RequestContext context) { String loginTicket = this.ticketIdGenerator.getNewTicketId("LT"); this.logger.debug("Generated login ticket {}", loginTicket); //放入RequestContext域 WebUtils.putLoginTicket(context, loginTicket); return "generated"; } public void setTicketIdGenerator(UniqueTicketIdGenerator generator) { this.ticketIdGenerator = generator; } }UniqueTicketIdGenerator要做的就是生成以LT作为前缀的loginTicket(例:LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn),并且把loginTicket放到RequestContext作用域中(LT只作为登录时使用的票据)。登录流程流转到第五个state(viewLoginForm)
<view-state id="viewLoginForm" view="casLoginView" model="credential"> <binder> <binding property="username" required="true"/> <binding property="password" required="true"/> </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>这样经过流程的流转登录界面展示在浏览器上,默认的用户界面是/WEB-INF/jsp/ui/default/ui/casLoginView.jsp,如果想要自定义登录界面可以参考这个。
默认的登录页面中有lt、execution和_eventId三个隐藏参数
<input type="hidden" name="lt" value="${loginTicket}" /> <input type="hidden" name="execution" value="${flowExecutionKey}" /> <input type="hidden" name="_eventId" value="submit" />lt参数值就是在GenerateLoginTicketAction的generate方法中生成的loginTicket
当用户输入了用户名和密码,点击登录按钮就会进入realSubmit这个state
<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="generateLoginTicket"/> </action-state>执行authenticationViaFormAction的submit方法
public class AuthenticationViaFormAction { public static final String SUCCESS = "success"; public static final String SUCCESS_WITH_WARNINGS = "successWithWarnings"; public static final String WARN = "warn"; public static final String AUTHENTICATION_FAILURE = "authenticationFailure"; public static final String ERROR = "error"; public static final String PUBLIC_WORKSTATION_ATTRIBUTE = "publicWorkstation"; protected final Logger logger = LoggerFactory.getLogger(this.getClass()); @NotNull private CentralAuthenticationService centralAuthenticationService; @NotNull private CookieGenerator warnCookieGenerator; public AuthenticationViaFormAction() { } public final Event submit(RequestContext context, Credential credential, MessageContext messageContext) { if (!this.checkLoginTicketIfExists(context)) { return this.returnInvalidLoginTicketEvent(context, messageContext); } else { return this.isRequestAskingForServiceTicket(context) ? this.grantServiceTicket(context, credential) : this.createTicketGrantingTicket(context, credential, messageContext); } } protected boolean checkLoginTicketIfExists(RequestContext context) { String loginTicketFromFlowScope = WebUtils.getLoginTicketFromFlowScope(context); String loginTicketFromRequest = WebUtils.getLoginTicketFromRequest(context); this.logger.trace("Comparing login ticket in the flow scope [{}] with login ticket in the request [{}]", loginTicketFromFlowScope, loginTicketFromRequest); //判断FlowScope和request中的loginTicket是否相同 return StringUtils.equals(loginTicketFromFlowScope, loginTicketFromRequest); } protected Event returnInvalidLoginTicketEvent(RequestContext context, MessageContext messageContext) { String loginTicketFromRequest = WebUtils.getLoginTicketFromRequest(context); this.logger.warn("Invalid login ticket [{}]", loginTicketFromRequest); messageContext.addMessage((new MessageBuilder()).error().code("error.invalid.loginticket").build()); return this.newEvent("error"); } protected boolean isRequestAskingForServiceTicket(RequestContext context) { //requestScope和FlowScope中获取TGT String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); FlowScope中获取service Service service = WebUtils.getService(context); return StringUtils.isNotBlank(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null; } protected Event grantServiceTicket(RequestContext context, Credential credential) { String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); try { Service service = WebUtils.getService(context); ServiceTicket serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service, new Credential[]{credential}); WebUtils.putServiceTicketInRequestScope(context, serviceTicketId); this.putWarnCookieIfRequestParameterPresent(context); return this.newEvent("warn"); } catch (AuthenticationException var6) { return this.newEvent("authenticationFailure", var6); } catch (TicketException var7) { if (var7 instanceof TicketCreationException) { this.logger.warn("Invalid attempt to access service using renew=true with different credential. Ending SSO session."); this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId); } return this.newEvent("error", var7); } } protected Event createTicketGrantingTicket(RequestContext context, Credential credential, MessageContext messageContext) { try { //根据用户凭证构造TGT,把TGT放到requestScope中,同时把TGT缓存到服务器的cache<ticketId,TGT>中 TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(new Credential[]{credential}); WebUtils.putTicketGrantingTicketInScopes(context, tgt); this.putWarnCookieIfRequestParameterPresent(context); this.putPublicWorkstationToFlowIfRequestParameterPresent(context); return this.addWarningMessagesToMessageContextIfNeeded(tgt, messageContext) ? this.newEvent("successWithWarnings") : this.newEvent("success"); } catch (AuthenticationException var5) { this.logger.debug(var5.getMessage(), var5); return this.newEvent("authenticationFailure", var5); } catch (Exception var6) { this.logger.debug(var6.getMessage(), var6); return this.newEvent("error", var6); } } protected boolean addWarningMessagesToMessageContextIfNeeded(TicketGrantingTicket tgtId, MessageContext messageContext) { boolean foundAndAddedWarnings = false; Iterator i$ = tgtId.getAuthentication().getSuccesses().entrySet().iterator(); while(i$.hasNext()) { Entry<String, HandlerResult> entry = (Entry)i$.next(); for(Iterator i$ = ((HandlerResult)entry.getValue()).getWarnings().iterator(); i$.hasNext(); foundAndAddedWarnings = true) { MessageDescriptor message = (MessageDescriptor)i$.next(); this.addWarningToContext(messageContext, message); } } return foundAndAddedWarnings; } private void putWarnCookieIfRequestParameterPresent(RequestContext context) { HttpServletResponse response = WebUtils.getHttpServletResponse(context); if (StringUtils.isNotBlank(context.getExternalContext().getRequestParameterMap().get("warn"))) { this.warnCookieGenerator.addCookie(response, "true"); } else { this.warnCookieGenerator.removeCookie(response); } } private void putPublicWorkstationToFlowIfRequestParameterPresent(RequestContext context) { if (StringUtils.isNotBlank(context.getExternalContext().getRequestParameterMap().get("publicWorkstation"))) { context.getFlowScope().put("publicWorkstation", Boolean.TRUE); } } private Event newEvent(String id) { return new Event(this, id); } private Event newEvent(String id, Exception error) { return new Event(this, id, new LocalAttributeMap("error", error)); } public final void setCentralAuthenticationService(CentralAuthenticationService centralAuthenticationService) { this.centralAuthenticationService = centralAuthenticationService; } public final void setWarnCookieGenerator(CookieGenerator warnCookieGenerator) { this.warnCookieGenerator = warnCookieGenerator; } /** @deprecated */ @Deprecated public void setTicketRegistry(TicketRegistry ticketRegistry) { this.logger.warn("setTicketRegistry() has no effect and will be removed in future CAS versions."); } private void addWarningToContext(MessageContext context, MessageDescriptor warning) { MessageBuilder builder = (new MessageBuilder()).warning().code(warning.getCode()).defaultText(warning.getDefaultMessage()).args(warning.getParams()); context.addMessage(builder.build()); } }AuthenticationViaFormAction的submit要做的就是判断FlowScope和request中的loginTicket是否相同。如果不同跳转到错误页面,如果相同,则根据用户凭证生成TGT(登录成功票据),并放到requestScope作用域中,同时把TGT缓存到服务器的cache<ticketId,TGT>中。登录流程流转到下个state(sendTicketGrantingTicket)
<action-state id="sendTicketGrantingTicket"> <evaluate expression="sendTicketGrantingTicketAction"/> <transition to="serviceCheck"/> </action-state>执行 sendTicketGrantingTicketAction 的 doExecute方法
protected Event doExecute(RequestContext context) { //requestScope和FlowScope中获取TGT String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); String ticketGrantingTicketValueFromCookie = (String)context.getFlowScope().get("ticketGrantingTicketId"); if (ticketGrantingTicketId == null) { return this.success(); } else { if (this.isAuthenticatingAtPublicWorkstation(context)) { 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 && this.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 { //response中添加TGC this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils.getHttpServletResponse(context), ticketGrantingTicketId); } if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) { this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie); } return this.success(); } }SendTicketGrantingTicketAction的doExecute要做的是获取TGT,并根据TGT生成cookie添加到response。登录流程流转到下一个state(serviceCheck)
<decision-state id="serviceCheck"> <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess"/> </decision-state>由于此时FlowScope中存在service(http://127.0.0.1:8081/cas-server/login?service=http%3A%2F%2Fapp1.example.com),登录流程流转到下一个state(generateServiceTicket)
<action-state id="generateServiceTicket"> <evaluate expression="generateServiceTicketAction"/> <transition on="success" to="warn"/> <transition on="authenticationFailure" to="handleAuthenticationFailure"/> <transition on="error" to="generateLoginTicket"/> <transition on="gateway" to="gatewayServicesManagementCheck"/> </action-state>执行generateServiceTicketAction的doExecute方法
protected Event doExecute(RequestContext context) { Service service = WebUtils.getService(context); String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context); try { //根据TGT和service生成service ticket(ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org) ServiceTicket serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket, service); ST放到requestScope中 WebUtils.putServiceTicketInRequestScope(context, serviceTicketId); return this.success(); } catch (TicketException var5) { if (var5 instanceof InvalidTicketException) { this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket); } return this.isGatewayPresent(context) ? this.result("gateway") : this.newEvent("error", var5); } }GenerateServiceTicketAction的doExecute要做的是获取service和TGT,并根据service和TGT生成以ST为前缀的serviceTicket(例:ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org),并把serviceTicket放到requestScope中。登录流程流转到下一个state(warn)
<decision-state id="warn"> <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/> </decision-state>FlowScope中不存在warnCookieValue,登录流程流转到下一个state(redirect)
<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>从requestScope中获取serviceTicket,构造response对象,并把response放到requestScope中。登录流程流转到下一个state(postRedirectDecision)
<decision-state id="postRedirectDecision"> <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView"/> </decision-state>由于request请求是get类型,登录流程流转到下一个state(redirectView)
<end-state id="redirectView" view="externalRedirect:#{requestScope.response.url}"/>此时流程如下:
跳转到应用系统(http://app1.example.com/?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)进入CAS客户端的AuthenticationFilter过滤器,由于session中获取名为_const_cas_assertion_的assertion对象不存在,但是request有ticket参数,所以进入到下一个过滤器TicketValidationFilter过滤器的validate方法通过httpClient访问CAS服务器端(http://127.0.0.1:8081/cas/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://app1.example.com)验证ticket是否正确,并返回assertion对象。 Assertion对象格式类似于 <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> <cas:authenticationSuccess> <cas:user>system</cas:user> </cas:authenticationSuccess> </cas:serviceResponse>至此就完成了登录的整个流程 参考文档:(https://blog.csdn.net/dovejing/article/details/44523545 https://www.ibm.com/developerworks/cn/education/java/j-spring-webflow/index.html)