前期準備:
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包。html
web.xml部分代碼java
訪問集成了CAS單點登陸的應用系統webapp1
下面講一下CAS單點登陸服務器端的登陸流程,流程的配置在/WEB-INF/login-webflow.xml文件中。web
/WEB-INF/login-webflow.xml部分代碼spring
- <var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
首先,設置一個變量,用來存儲用戶名和密碼信息。express
- <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中。cookie
/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml部分代碼session
- <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中。app
/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;
- }
-
- context.getFlowScope().put(
- "ticketGrantingTicketId", this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
-
- context.getFlowScope().put(
- "warnCookieValue", Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
-
- final Service service = WebUtils.getService(this.argumentExtractors, context);
-
- if (service != null && logger.isDebugEnabled()) {
- logger.debug("Placing service in FlowScope: " + service.getId());
- }
-
- 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);
-
- 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) {
-
- final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);
- this.logger.debug("Generated login ticket " + loginTicket);
- WebUtils.putLoginTicket(context, loginTicket);
- 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);
-
- 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 {
-
- final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);
- final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);
-
- 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";
- }
-
- final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
-
- 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 {
-
- 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) {
-
- final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
- final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");
-
- if (ticketGrantingTicketId == null) {
- return success();
- }
-
- 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) {
-
- final Service service = WebUtils.getService(context);
-
- final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
-
- try {
-
- final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket,
- service);
-
- 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對象。
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的流轉。
http://blog.csdn.net/dovejing/article/details/44523545