1. 程式人生 > >CAS 4.1.10 版本服務端原始碼解讀

CAS 4.1.10 版本服務端原始碼解讀

在工作中經常會對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>

二、原始碼解讀

2.1 訪問CAS服務端進行認證

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進行處理。

2.2 SpringMVC與Spring WebFlow整合

在上面的servlet配置中會發現,其核心的配置檔案是/WEB-INF/cas-servlet.xml, /WEB-INF/cas-servlet-*.xml。SpringMVC在初始化的時候會去自動載入cas-servlet.xmlcas-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檔案對映成檢視名稱。

2.3 CAS 登入流程解析

由上面的分析知道,登入流程的配置檔案是/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繼承自AbstractActionAbstractAction方法是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" />

它會去執行ticketGrantingTicketCheckdoExecute方法檢查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().