CAS單點登入原始碼解析之【伺服器端】
前期準備:
2.應用系統webapp1(http://127.0.0.1:8090/webapp1/main.do) 3.應用系統webapp2(http://127.0.0.1:8091/webapp2/main.do) 4.CAS單點登入伺服器端(http://127.0.0.1:8081/cas-server/) 本次討論包括CAS單點登入伺服器端的部分原始碼,以及在此基礎上進行二次開發,因此需要修改部分CAS伺服器端的原始碼,原始碼部分的修改在下面進行討論。關於CAS客戶端的原始碼分析,請參考另一篇文章http://blog.csdn.net/dovejing/article/details/44426547
其中cas-server-3.5.2-release.zip為CAS伺服器端的原始碼zip包。
web.xml部分程式碼
<servlet> <servlet-name>cas</servlet-name> <servlet-class>org.jasig.cas.web.init.SafeDispatcherServlet</servlet-class> <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> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/logout</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/validate</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/serviceValidate</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/samlValidate</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/proxy</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/proxyValidate</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/CentralAuthenticationService</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/services/add.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/services/viewStatistics.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/services/logout.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/services/loggedOut.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/services/manage.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/services/edit.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/openid/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/services/deleteRegisteredService.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/services/updateRegisteredServiceEvaluationOrder.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/status</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/authorizationFailure.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/403.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/error</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/authcode</url-pattern> </servlet-mapping>
訪問集成了CAS單點登入的應用系統webapp1
下面講一下CAS單點登入伺服器端的登入流程,流程的配置在/WEB-INF/login-webflow.xml檔案中。
/WEB-INF/login-webflow.xml部分程式碼
<var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
首先,設定一個變數,用來儲存使用者名稱和密碼資訊。
<on-start> <evaluate expression="initialFlowSetupAction" /> </on-start>
整個登入流程從此處開始,流程初始化initialFlowSetupAction的配置資訊在/WEB-INF/cas-servlet.xml中。
/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"/>
其中argumentExtractors配置檔案在/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml中。
/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml部分程式碼
<bean
id="casArgumentExtractor"
class="org.jasig.cas.web.support.CasArgumentExtractor"
p:httpClient-ref="noRedirectHttpClient"
p:disableSingleSignOut="${slo.callbacks.disabled:false}" />
<bean id="samlArgumentExtractor" class="org.jasig.cas.web.support.SamlArgumentExtractor"
p:httpClient-ref="noRedirectHttpClient"
p:disableSingleSignOut="${slo.callbacks.disabled:false}" />
<util:list id="argumentExtractors">
<ref bean="casArgumentExtractor" />
<ref bean="samlArgumentExtractor" />
</util:list>
其中warnCookieGenerator配置檔案在/WEB-INF/spring-configuration/warnCookieGenerator.xml中。/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配置檔案在/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml中。/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml部分程式碼
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="-1"
p:cookieName="CASTGC"
p:cookiePath="/cas" />
初始化部分會呼叫InitialFlowSetupAction的doExecute方法,如果有特殊需求,可以在此方法中增加相應的邏輯。如果希望單點登入整合統一身份認證,那麼可以在此處增加統一身份認證的邏輯。關於CAS單點登入與統一身份認證的整合,我會單獨寫一篇。
InitialFlowSetupAction的doExecute方法
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;
}
//將TGT放在FlowScope作用域中
context.getFlowScope().put(
"ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
//將warnCookieValue放在FlowScope作用域中
context.getFlowScope().put(
"warnCookieValue", Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
//獲取service引數
final Service service = WebUtils.getService(this.argumentExtractors, context);
if (service != null && logger.isDebugEnabled()) {
logger.debug("Placing service in FlowScope: " + service.getId());
}
//將service放在FlowScope作用域中
context.getFlowScope().put("service", service);
return result("success");
}
InitialFlowSetupAction的doExecute要做的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域中,以便在登入流程中的state中進行判斷。初始化完成後,登入流程流轉到第一個state(ticketGrantingTicketExistsCheck)。
<decision-state id="ticketGrantingTicketExistsCheck">
<if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
</decision-state>
當我們第一次訪問集成了CAS單點登入的應用系統webapp1時(http://127.0.0.1:8090/webapp1/main.do),此時應用系統會跳轉到CAS單點登入的伺服器端(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do)。此時,request的cookies中不存在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不存在,登入流程流轉到第三個state(serviceAuthorizationCheck)。
<action-state id="serviceAuthorizationCheck">
<evaluate expression="serviceAuthorizationCheck"/>
<transition to="generateLoginTicket"/>
</action-state>
ServiceAuthorizationCheck的doExecute方法
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();
}
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
if (registeredService == null) {
logger.warn("Unauthorized Service Access for Service: [ {} ] - service is not defined in the service registry.", service.getId());
throw new UnauthorizedServiceException();
}
else if (!registeredService.isEnabled()) {
logger.warn("Unauthorized Service Access for Service: [ {} ] - service is not enabled in the service registry.", service.getId());
throw new UnauthorizedServiceException();
}
return success();
}
ServiceAuthorizationCheck的doExecute方法,要做的就是判斷FlowScope作用域中是否存在service,如果service存在,查詢service的註冊資訊。登入流程流轉到第四個state(generateLoginTicket)。
<action-state id="generateLoginTicket">
<evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" />
<transition on="generated" to="viewLoginForm" />
</action-state>
/WEB-INF/cas-servlet.xml部分程式碼<bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction"
p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator" />
/WEB-INF/spring-configuration/uniqueIdGenerators.xml部分程式碼<bean id="loginTicketUniqueIdGenerator" class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator">
<constructor-arg
index="0"
type="int"
value="30" />
</bean>
DefaultUniqueTicketIdGenerator要做的就是生成以LT作為字首的loginTicket(例:LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn)。注:LT只作為登入時使用的票據。
GenerateLoginTicketAction的generate方法
public final String generate(final RequestContext context) {
//LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn
final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);//生成loginTicket
this.logger.debug("Generated login ticket " + loginTicket);
WebUtils.putLoginTicket(context, loginTicket);//放到flowScope中
return "generated";
}
GenerateLoginTicketAction的generate要做的就是生成loginTicket,並且把loginTicket放到FlowScope作用域中。登入流程流轉到第五個state(viewLoginForm)。<view-state id="viewLoginForm" view="casLoginView" model="credentials">
<binder>
<binding property="username" />
<binding property="password" />
</binder>
<on-entry>
<set name="viewScope.commandName" value="'credentials'" />
</on-entry>
<transition on="submit" bind="true" validate="true" to="realSubmit">
<evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
</transition>
</view-state>
至此,經過五個state的流轉,我們完成了第一次訪問集成了單點登入的應用系統,此時流轉到CAS單點登入伺服器端的登入頁面/WEB-INF/jsp/ui/default/casLoginView.jsp。由於casLoginView.jsp是CAS提供的預設登入頁面,需要把此頁面修改成我們系統需要的登入頁面,格式需要參考casLoginView.jsp。
注意,預設的登入頁面中有lt、execution和_eventId三個隱藏引數,lt引數值就是在GenerateLoginTicketAction的generate方法中生成的loginTicket。
<input type="hidden" name="lt" value="${loginTicket}" />
<input type="hidden" name="execution" value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
下面說一下CAS單點登入伺服器端的登入驗證
當輸入使用者名稱和密碼,點選登入按鈕時,會執行AuthenticationViaFormAction的doBind方法。
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
p:centralAuthenticationService-ref="centralAuthenticationService"
p:warnCookieGenerator-ref="warnCookieGenerator" />
AuthenticationViaFormAction的doBind方法
public final void doBind(final RequestContext context, final Credentials credentials) throws Exception {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
//bean中沒有注入,這裡什麼也不做
if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) {
this.credentialsBinder.bind(request, credentials);
}
}
登入流程流轉到第一個state(realSubmit),會執行AuthenticationViaFormAction的submit方法。<action-state id="realSubmit">
<evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />
<transition on="warn" to="warn" /><!-- 警告,轉向其他站點前提示我 -->
<transition on="success" to="sendTicketGrantingTicket" /><!-- 成功 -->
<transition on="error" to="generateLoginTicket" /><!-- 錯誤 -->
<transition on="accountDisabled" to="casAccountDisabledView" />
<transition on="mustChangePassword" to="casMustChangePassView" />
<transition on="accountLocked" to="casAccountLockedView" />
<transition on="badHours" to="casBadHoursView" />
<transition on="badWorkstation" to="casBadWorkstationView" />
<transition on="passwordExpired" to="casExpiredPassView" />
</action-state>
AuthenticationViaFormAction的submit方法
public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext)
throws Exception {
// Validate login ticket
final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);
final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);
//判斷FlowScope和request中的loginTicket是否相同
if (!authoritativeLoginTicket.equals(providedLoginTicket)) {
this.logger.warn("Invalid login ticket " + providedLoginTicket);
final String code = "INVALID_TICKET";
messageContext.addMessage(new MessageBuilder().error().code(code).arg(providedLoginTicket).defaultText(code).build());
return "error";
}
//requestScope和FlowScope中獲取TGT
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
//FlowScope中獲取service
final Service service = WebUtils.getService(context);
if (StringUtils.hasText(context.getRequestParameters().get("renew"))
&& ticketGrantingTicketId != null && service != null) {
try {
final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(
ticketGrantingTicketId, service, credentials);
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
putWarnCookieIfRequestParameterPresent(context);
return "warn";
} catch (final TicketException e) {
if (isCauseAuthenticationException(e)) {
populateErrorsInstance(e, messageContext);
return getAuthenticationExceptionEventId(e);
}
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
if (logger.isDebugEnabled()) {
logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e);
}
}
}
try {
//根據使用者憑證構造TGT,把TGT放到requestScope中,同時把TGT快取到伺服器的cache<ticketId,TGT>中
WebUtils.putTicketGrantingTicketInRequestScope(context,
this.centralAuthenticationService.createTicketGrantingTicket(credentials));
putWarnCookieIfRequestParameterPresent(context);
return "success";
} catch (final TicketException e) {
populateErrorsInstance(e, messageContext);
if (isCauseAuthenticationException(e))
return getAuthenticationExceptionEventId(e);
return "error";
}
}
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(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();
}
//response中新增TGC
this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils
.getHttpServletResponse(context), ticketGrantingTicketId);
if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
this.centralAuthenticationService
.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
}
return 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://127.0.0.1:8090/webapp1/main.do),登入流程流轉到第四個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>
GenerateServiceTicketAction的doExecute方法
protected Event doExecute(final RequestContext context) {
//獲取service
final Service service = WebUtils.getService(context);
//獲取TGT
final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
try {
//根據TGT和service生成service ticket(ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org)
final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket,
service);
//ST放到requestScope中
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
return success();
} catch (final TicketException e) {
if (isGatewayPresent(context)) {
return result("gateway");
}
}
return error();
}
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請求(http://127.0.0.1:8081/cas-server/login?service=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_”的assertion物件不存在,但是request有ticket引數,所以進入到下一個過濾器。
- TicketValidationFilter過濾器的validate方法通過httpClient訪問CAS伺服器端(http://127.0.0.1:8081/cas-server/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://127.0.0.1:8090/webapp1/main.do)驗證ticket是否正確,並返回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)。InitialFlowSetupAction的doExecute初始化完成後,登入流程流轉到第一個state(ticketGrantingTicketExistsCheck)。
<decision-state id="ticketGrantingTicketExistsCheck">
<if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
</decision-state>
因為應用系統webapp1已經成功登入,所以request的cookies中存在TGT,並儲存到FlowScope中,登入流程流轉到第二個state(hasServiceCheck)。<decision-state id="hasServiceCheck">
<if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" />
</decision-state>
FlowScope中存在service,登入流程流轉到第三個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相同,請參考前面webapp1的流轉。