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
<%@ 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.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檔案對映成檢視名稱。
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
繼承自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().