回到顶部


CAS服务端流程分析

'CAS单点登录服务器端的登录流程'

-----流程的配置在/WEB-INF/login-webflow.xml文件中
 <var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>
-----首先,设置一个变量,用来存储用户名和密码信息
 <on-start>
    <evaluate expression="initialFlowSetupAction"/>
 </on-start>
 
-------从此开始整个登录流程,initialFlowSetupAction,的配置信息/WEB-INF/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}"  />

---------'argumentExtractors'
其中'argumentExtractors'配置文件在/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml           
   <bean id="casArgumentExtractor"  class="org.jasig.cas.web.support.CasArgumentExtractor"/>
   <util:list id="argumentExtractors">
        <!-- <ref bean="samlArgumentExtractor" /> -->
        <ref bean="casArgumentExtractor"/>
    </util:list>           

------'warnCookieGenerator'
其中warnCookieGenerator的配置文件在/WEB-INF/spring-configuration/warnCookieGenerator.xml
    <bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
          p:cookieSecure="true"
          p:cookieMaxAge="-1"
          p:cookieName="CASPRIVACY"
          p:cookiePath="/cas"/>

------'ticketGrantingTicketCookieGenerator'
其中ticketGrantingTicketCookieGenerator的配置文件在/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml
<bean id="ticketGrantingTicketCookieGenerator"
          class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
          c:casCookieValueManager-ref="cookieValueManager"
          p:cookieSecure="false"
          p:cookieMaxAge="-1"
          p:cookieName="TGC"
          p:cookiePath="/cas"/>
          
初始化部分会调用initialFlowSetupAction的doExecute方法,如果有特殊需求,可以在此方法中增加相应的逻辑。
-->InitialFlowSetupAction--doExecute

 /**
  * doExecute的目的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域
  * 以便在登陆流程中的state进行判断,初始化完成后,登陆流程转到第一个state-->ticketGrantingTicketExistsCheck
  */
 protected Event doExecute(final RequestContext context) throws Exception {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
        if (!this.pathPopulated) {
            final String contextPath = context.getExternalContext().getContextPath();
            final String cookiePath = StringUtils.hasText(contextPath) ? contextPath + '/' : "/";
            logger.info("Setting path for cookies to: {} ", cookiePath);
            this.warnCookieGenerator.setCookiePath(cookiePath);
            this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
            this.pathPopulated = true;
        }
        //获取并设置ticketGrantingTicketId,即TGT,用于证明用户已经登录
        WebUtils.putTicketGrantingTicketInScopes(context, this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
        //将warnCookieValue放在FlowScope中
        WebUtils.putWarningCookie(context, Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
        //获取service参数,即用户重定向/cas/login之前访问的URL
        final Service service = WebUtils.getService(this.argumentExtractors, context);
        if (service != null) {
            logger.debug("Placing service in context scope: [{}]", service.getId());
            final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
            if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) {
                logger.debug("Placing registered service [{}] with id [{}] in context scope",
                        registeredService.getServiceId(),
                        registeredService.getId());
                WebUtils.putRegisteredService(context, registeredService);
            }
        } else if (!this.enableFlowOnAbsentServiceRequest) {
            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"));
        }
        //将service放在FlowScope的作用域中
        WebUtils.putService(context, service);
        //成功初始化,进入下一个流程
        return result("success");
    }
    
----初始化完成后,登陆流程转到第一个state,ticketGrantingTicketCheck /WEB-INF/webflow/login/login-webfolw.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>
    
    
 --->TicketGrantingTicketCheckAction-->doExecute   
    @Override
    protected Event doExecute(final RequestContext requestContext) throws Exception {
        final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
        if (!StringUtils.hasText(tgtId)) {
            return new Event(this, NOT_EXISTS);
        }
        String eventId = INVALID;
        try {
            final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
            if (ticket != null && !ticket.isExpired()) {
                eventId = VALID;
            }
        } catch (final TicketException e) {
            logger.trace("Could not retrieve ticket id {} from registry.", e);
        }
        return new Event(this,  eventId);
    }
  当我们第一次访问集成了CAS单点登录的应用系统WEBAPP1时(http://127.0.0.1:8090/webapp1/main.do) 此时应系统会跳转到CAS的单点登录
  的服务器端(http://127.0.0.1:/8090/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do)
  由于此时request的cookie中不存在CASTGC(TGT),因此FlowScope作用域中的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保存在了FlowScope作用域中,但request中的参数gateway不存在,登录流程流转到第三个节点serviceAuthorizationCheck

----登录流转到serviceAuthorizationCheck阶段
    //这步的目的就是,在登陆前做一个服务授权检查
    <action-state id="serviceAuthorizationCheck">
        <evaluate expression="serviceAuthorizationCheck"/>
        <transition to="generateLoginTicket"/>
    </action-state>   

  --->ServiceAuthorizationCheck-->doExecte--
   /** 目的就是判断FlowScope作用域是否存在service,如果service存在,查找service的注册信息。登录流程转到第四个state(generateLoginTicket) */
    @Override
    protected Event doExecute(final RequestContext context) throws Exception {
        final Service service = WebUtils.getService(context);
        //No service == plain /login request. Return success indicating transition to the login form
        if (service == null) { return success(); }
        if (this.servicesManager.getAllServices().isEmpty()) {
            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);
        }
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
        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);
        }
        return success();
    }


---登录流转到:generateLoginTicket
   <action-state id="generateLoginTicket">
        <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)"/>
        <transition on="generated" to="viewLoginForm"/>
   </action-state>
 '-----generateLoginTicket---/WEB-INF/cas-servlet.xml部分代码------'
   <bean id="generateLoginTicketAction" 
        class="org.jasig.cas.web.flow.GenerateLoginTicketAction"
        p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator"/>
 '-----loginTicketUniqueIdGenerator---/WEB-INF/spring-configuration/uniquieIdGenerator.xml---' 
   <bean id="loginTicketUniqueIdGenerator"
          class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator"
          c:maxLength="30"
          c:suffix="${host.name}" />
  DefaultUniqueTicketIdGenerator的作用就是生成以LT作为前缀的loginTicket(例如:LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn),LT只作为登录使用的票据。
  //--->GenerateLoginTicketAction的generate方法,该方法的主要目的就是loginTicket放到FlowScope作用域中
  public final String generate(final RequestContext context) {
       //通过DefaultUniqueTicketGenerator生成loginTicket,可以通过实现实现UniqueTicketIdGenerator.java,来自定义生成loginTicket的格式
        final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX); //这里ticketIdGerator就是DefaultUniqueTicketIdGenerator
        logger.debug("Generated login ticket {}", loginTicket);
        //ticket放入FlowScope
        WebUtils.putLoginTicket(context, loginTicket);
        return "generated";
    }
    
-----登录流转到: 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'"/>
        </on-entry>
        <transition on="submit" bind="true" validate="true" to="realSubmit"/>
    </view-state>
    
  到此,经过5个state 的流转,就完成了第一次访问集成单点登录的应用系统,此时流转到CAS单点登录服务器的登录页面/WEB-INF/jsp/ui/default/casLoginView.jsp
  由于casLoginView.jsp是CAS提供的默认登录页面,需要把此页面修改为我们系统需要的登录页面,格式需要参考casLoginView.jsp;
  注意,默认的登录页面中有lt,execution和_eventId三个隐藏参数,It参数值就是GenerateLoginTicketAction的generate方法中生成的loginTicket.
  
-----casLoginView.jsp
  <input type="hidden" name="lt" value="${loginTicket}" />
  <input type="hidden" name="execution" value="${flowExecutionKey}" />
  <input type="hidden" name="_eventId" value="submit" />

        

cas 服务端的登录认证

当我们在登录页面输入用户名和密码,点击登录后会执行AuthenticationViaFormAction的doBind方法
---配置文件/WEB-INF/cas-servlet.xml----
  <bean id="authenticationViaFormAction" 
        class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
        p:centralAuthenticationService-ref="centralAuthenticationService"
        p:warnCookieGenerator-ref="warnCookieGenerator"/>

--------流程跳转到---------------
   <action-state id="realSubmit">
        <evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)"/>
        <transition on="warn" to="warn"/>
        <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 final Event submit(final RequestContext context, final Credential credential,
                              final MessageContext messageContext)  {
        //判断FlowScope和request域中的loginTicket是否相同
        if (!checkLoginTicketIfExists(context)) {
            return returnInvalidLoginTicketEvent(context, messageContext);
        }
        //根据用户凭证生成TGT(登录成功票据),并放到requestScope作用域中,同时把TGT缓存到服务器cache<ticketId,TGT>中
        if (isRequestAskingForServiceTicket(context)) {
            return grantServiceTicket(context, credential);
        }
        return createTicketGrantingTicket(context, credential, messageContext);
    }  
  ----登录流程跳转到第二个state-----           
     <action-state id="sendTicketGrantingTicket">
        <evaluate expression="sendTicketGrantingTicketAction"/>
        <transition to="serviceCheck"/>
     </action-state>   
     
   --->SendTicketGrantingTicketAction的doExecute的方法
    /*** SendTicketGrantingTicketAction的要做的获取TGT,并根据TGT生成cookie添加到response*/
    protected Event doExecute(final RequestContext context) {
        //获取requestScope和FlowScope中的TGT
        final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
        final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");
        if (ticketGrantingTicketId == null) {   return success();  }
        if (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 && 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 success();
    }
------登录流程跳转到第三个state            
    <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://127.0.0.1:8090/webapp1/main.do)
------登录流程跳转到第四个state
     <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要做的是获取service和TGT
    * 并根据service和TGT生成以ST为前缀的serviceTicket,并把serviceTicket放到requestScope中
    * */
    @Override
    protected Event doExecute(final RequestContext context) {
        //获取service
        final Service service = WebUtils.getService(context);
        //获取TGT
        final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
        try {
            final Credential credential = WebUtils.getCredential(context);
            //根据TGT和service ticket(ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org)
            final ServiceTicket serviceTicketId = this.centralAuthenticationService
                .grantServiceTicket(ticketGrantingTicket, service, credential);
            //ST放到requestScope中
            WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
            return success();
        } catch (final AuthenticationException e) {
            logger.error("Could not verify credentials to grant service ticket", e);
        } catch (final TicketException e) {
            if (e instanceof InvalidTicketException) {
                this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
            }
            if (isGatewayPresent(context)) {
                return result("gateway");
            }
        }
        return error();
    }
    
------登录流程跳转到第五个state            
      <decision-state id="warn">
        <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/>
      </decision-state>      
      由于此时FlowScope中不存在warnCookieValue,所以跳转到redirect

------登录流程跳转到第六个state
    <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>
    在这一步从reuqestScope中获取serviceTicket,构造response对象,并把response,放到requestScope中
    
-----登录流程流转到第七个state(postRedirectDecision)
     <decision-state id="postRedirectDecision">
        <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView"/>
     </decision-state>
     由于request请求(http://127.0.0.1:8081/cas-server/login?server=http://127.0.0.1:8090/webapp1/main.do)是get类型
     登录流程流转到第八个state(redirectView)
     <end-state id="redirectView" view="externalRedirect:#{requestScope.response.url}"/>
         
----------
     此时流程如下:
      > 跳转到应用系统(http://127.0.0.1:8090/webapp1/main.do?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)
      > 进入cas客户端的AuthenticationFilter获取器,由于session中获取名为"_const_cas_assertion_"的assert对象不存在,但request有ticket,所以进入下一个过滤器
      > TicketValidationFilter过滤器的validate方法通过httpclient访问CAS服务器
         http://127.0.0.1:8081/cas-server/serviceValidate?ticket=ST-1-4hHxxxxcas01.example.org&service=http://127.0.0.1:8090/webapp1/main.do
         验证ticket是否正确,并返回assertion对象。
         
---------Assertion对象的格式类似
     <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>  
         <cas:authenticationSuccess>  
            <cas:user>system</cas:user>  
         </cas:authenticationSuccess>      
     </cas:serviceResponse>  

访问集成了CAS单点登录的应用系统webApp2

当我们第一次访问集成了CAS单点登录的应用系统webapp2时(http://127.0.0.1:8091/webapp2/main.do),此时应用系统会跳转到CAS单点登录的
服务器(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8091/webapp2/main.do)
   InitialFlowStepAction的doExecutor初始化完成后,登录流程流转到第一个state
------登录流程跳转到第一个state:ticketGrantingTicketExistsCheck
  <decision-state id="ticketGrantingTicketExistsCheck">  
    <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />  
  </decision-state> 
  因为应用系统webapp1已经成功登录,所以request的cookies中存在TGT,并保存到FlowScope中登录流转到第二个state
  
------登录流转到第二个state:hasServiceCheck
  <decision-state id="hasServiceCheck">  
    <if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" />  
  </decision-state>  
  FlowScope中存在service,登录流转到第三个state(renewRequestCheck)
     
------登录流转到第二个state:hasServiceCheck
  <decision-state id="hasServiceCheck">  
    <if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" />  
  </decision-state>  
------登录流转到第三个state:renewRequestCheck
   <decision-state id="renewRequestCheck">  
       <if test="requestParameters.renew != '' and requestParameters.renew != null"   
        then="serviceAuthorizationCheck" else="generateServiceTicket" />  
   </decision-state>  

-------request中不存在renew,登录流程流转到第四个state:generateServiceTicket
   <action-state id="generateServiceTicket">  
    <evaluate expression="generateServiceTicketAction" />  
    <transition on="success" to ="warn" />  
    <transition on="error" to="generateLoginTicket" />  
    <transition on="gateway" to="gatewayServicesManagementCheck" />  
   </action-state>  
----后续的流转与应用系统webapp1相同---    
posted on 2018-04-13 05:35  ssgao  阅读(1304)  评论(0编辑  收藏  举报