以前文章講到了怎麼利用jasig CAS實現sso: html
http://my.oschina.net/indestiny/blog/200768 前端
本文對jasig CAS驗證過程作個簡單的分析,便於之後可以更好定製本身的CAS, 要了解CAS流程你須要知道spring,springmvc等知識,也要了解spring-webflow, 由於整個驗證流程都是由spring-webflow定製的,你能夠參考我轉載的一篇spring-webflow的文章: java
http://my.oschina.net/indestiny/blog/201988 web
ok, 就開始了。 spring
重點就是服務器端的配置:WEB-INF/login-webflow.xml中,它定義了整個登陸流程,咱們先就分析其流程: 數據庫
<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"> <var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" /> <on-start> <evaluate expression="initialFlowSetupAction" /> </on-start> <decision-state id="ticketGrantingTicketExistsCheck"> <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" /> </decision-state> <decision-state id="gatewayRequestCheck"> <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null" then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck" /> </decision-state> <decision-state id="hasServiceCheck"> <if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" /> </decision-state> <decision-state id="renewRequestCheck"> <if test="requestParameters.renew != '' and requestParameters.renew != null" then="serviceAuthorizationCheck" else="generateServiceTicket" /> </decision-state> <!-- Do a service authorization check early without the need to login first --> <action-state id="serviceAuthorizationCheck"> <evaluate expression="serviceAuthorizationCheck"/> <transition to="generateLoginTicket"/> </action-state> <!-- The "warn" action makes the determination of whether to redirect directly to the requested service or display the "confirmation" page to go back to the server. --> <decision-state id="warn"> <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect" /> </decision-state> <!-- <action-state id="startAuthenticate"> <action bean="x509Check" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="warn" to="warn" /> <transition on="error" to="generateLoginTicket" /> </action-state> --> <!-- LPPE transitions begin here: You will also need to move over the 'lppe-configuration.xml' file from the 'unused-spring-configuration' folder to the 'spring-configuration' folder so CAS can pick up the definition for the bean 'passwordPolicyAction'. --> <action-state id="passwordPolicyCheck"> <evaluate expression="passwordPolicyAction" /> <transition on="showWarning" to="passwordServiceCheck" /> <transition on="success" to="sendTicketGrantingTicket" /> <transition on="error" to="viewLoginForm" /> </action-state> <action-state id="passwordServiceCheck"> <evaluate expression="sendTicketGrantingTicketAction" /> <transition to="passwordPostCheck" /> </action-state> <decision-state id="passwordPostCheck"> <if test="flowScope.service != null" then="warnPassRedirect" else="pwdWarningPostView" /> </decision-state> <action-state id="warnPassRedirect"> <evaluate expression="generateServiceTicketAction" /> <transition on="success" to="pwdWarningPostView" /> <transition on="error" to="generateLoginTicket" /> <transition on="gateway" to="gatewayServicesManagementCheck" /> </action-state> <end-state id="pwdWarningAbstractView"> <on-entry> <set name="flowScope.passwordPolicyUrl" value="passwordPolicyAction.getPasswordPolicyUrl()" /> </on-entry> </end-state> <end-state id="pwdWarningPostView" view="casWarnPassView" parent="#pwdWarningAbstractView" /> <end-state id="casExpiredPassView" view="casExpiredPassView" parent="#pwdWarningAbstractView" /> <end-state id="casMustChangePassView" view="casMustChangePassView" parent="#pwdWarningAbstractView" /> <end-state id="casAccountDisabledView" view="casAccountDisabledView" /> <end-state id="casAccountLockedView" view="casAccountLockedView" /> <end-state id="casBadHoursView" view="casBadHoursView" /> <end-state id="casBadWorkstationView" view="casBadWorkstationView" /> <!-- LPPE transitions end here... --> <action-state id="generateLoginTicket"> <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" /> <transition on="generated" to="viewLoginForm" /> </action-state> <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> <action-state id="realSubmit"> <evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" /> <!-- To enable LPPE on the 'warn' replace the below transition with: <transition on="warn" to="passwordPolicyCheck" /> CAS will attempt to transition to the 'warn' when there's a 'renew' parameter and there exists a ticketGrantingId and a service for the incoming request. --> <transition on="warn" to="warn" /> <!-- To enable LPPE on the 'success' replace the below transition with: <transition on="success" to="passwordPolicyCheck" /> --> <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> <action-state id="sendTicketGrantingTicket"> <evaluate expression="sendTicketGrantingTicketAction" /> <transition to="serviceCheck" /> </action-state> <decision-state id="serviceCheck"> <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess" /> </decision-state> <action-state id="generateServiceTicket"> <evaluate expression="generateServiceTicketAction" /> <transition on="success" to ="warn" /> <transition on="error" to="generateLoginTicket" /> <transition on="gateway" to="gatewayServicesManagementCheck" /> </action-state> <action-state id="gatewayServicesManagementCheck"> <evaluate expression="gatewayServicesManagementCheck" /> <transition on="success" to="redirect" /> </action-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> <decision-state id="postRedirectDecision"> <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView" /> </decision-state> <!-- the "viewGenericLogin" is the end state for when a user attempts to login without coming directly from a service. They have only initialized their single-sign on session. --> <end-state id="viewGenericLoginSuccess" view="casLoginGenericSuccessView" /> <!-- The "showWarningView" end state is the end state for when the user has requested privacy settings (to be "warned") to be turned on. It delegates to a view defines in default_views.properties that display the "Please click here to go to the service." message. --> <end-state id="showWarningView" view="casLoginConfirmView" /> <end-state id="postView" view="postResponseView"> <on-entry> <set name="requestScope.parameters" value="requestScope.response.attributes" /> <set name="requestScope.originalUrl" value="flowScope.service.id" /> </on-entry> </end-state> <!-- The "redirect" end state allows CAS to properly end the workflow while still redirecting the user back to the service required. --> <end-state id="redirectView" view="externalRedirect:${requestScope.response.url}" /> <end-state id="viewServiceErrorView" view="viewServiceErrorView" /> <end-state id="viewServiceSsoErrorView" view="viewServiceSsoErrorView" /> <global-transitions> <!-- CAS-1023 This one is simple - redirects to a login page (same as renew) when 'ssoEnabled' flag is unchecked instead of showing an intermediate unauthorized view with a link to login page --> <transition to="viewLoginForm" on-exception="org.jasig.cas.services.UnauthorizedSsoServiceException"/> <transition to="viewServiceErrorView" on-exception="org.springframework.webflow.execution.repository.NoSuchFlowExecutionException" /> <transition to="viewServiceErrorView" on-exception="org.jasig.cas.services.UnauthorizedServiceException" /> </global-transitions> </flow>首先設置了一個變量 credentials來保存用戶名及密碼信息:
<var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />在該flow執行一開始,作一次初始化:
<on-start> <evaluate expression="initialFlowSetupAction" /> </on-start>
對應其配置在/WEB-INF/cas-servlet.xml中: express
<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中: 瀏覽器
<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>
其中ticketGrantingTicketCookieGenerator配置在/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml: 服務器
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="true" p:cookieMaxAge="-1" p:cookieName="CASTGC" p:cookiePath="/cas" />
其中warnCookieGenerator的配置在/WEB-INF/spring-configuration/warnCookieGenerator.xml: cookie
<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="true" p:cookieMaxAge="-1" p:cookieName="CASPRIVACY" p:cookiePath="/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); context.getFlowScope().put("service", service); return result("success"); }
講完初始化flow配置,看看第一個state(ticketGrantingTicketExistsCheck), 當第一次登陸cas時(https://cas_server:8443/cas/login), 沒有ticketGrantingTicketId, 因此會留向gatewayRequestCheck state:
<decision-state id="ticketGrantingTicketExistsCheck"> <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" /> </decision-state>看gatewayRequestCheck state,第一次service也是爲null, 因此流向serviceAuthorizationCheck state:
<decision-state id="gatewayRequestCheck"> <if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null" then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck" /> </decision-state>繼續看serviceAuthorizationCheck state, 其會先調用 org.jasig.cas.web.flow.ServiceAuthorizationCheck的doExecute方法,以後流向generateLoginTicket,生成ticket:
<action-state id="serviceAuthorizationCheck"> <evaluate expression="serviceAuthorizationCheck"/> <transition to="generateLoginTicket"/> </action-state>看generateLoginTicket state, 調用generateLoginTicketAction.generate方法來生成ticket,返回給客戶端:
<action-state id="generateLoginTicket"> <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" /> <transition on="generated" to="viewLoginForm" /> </action-state>從CAS server debug信息和個人請求信息來看,server先生成這個ticket,返回給瀏覽器,當咱們登陸時,會帶上這個ticket:
我登陸時請求信息:
仍是看看ticket怎麼生成的吧,generateLoginTicketAction bean:
<bean id="generateLoginTicketAction" class="org.jasig.cas.web.flow.GenerateLoginTicketAction" p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator"/>/WEB-INF/spring-configuration/uniqueIdGenerators.xml定義了不少Generator, 好比上面的LoginTicketUniqueIdGenerator:
<bean id="loginTicketUniqueIdGenerator" class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator"> <constructor-arg index="0" type="int" value="30" /> </bean>接着看GenerateLoginTicketAction的generate方法:
public class GenerateLoginTicketAction { /** 3.5.1 - Login tickets SHOULD begin with characters "LT-" */ private static final String PREFIX = "LT"; @NotNull private UniqueTicketIdGenerator ticketIdGenerator; public final String generate(final RequestContext context) { final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX);//調用generator生成 this.logger.debug("Generated login ticket " + loginTicket); WebUtils.putLoginTicket(context, loginTicket);//最終放到flowScope中 return "generated"; } ... }
生成以後,就流向viewLoginForm state,其view未casLoginView,對應就是/WEB-INF/jsp/ui/default/casLoginView.jsp了:
<view-state id="viewLoginForm" view="casLoginView" model="credentials"> <binder><!-- 綁定html form表單中的用戶名及密碼 --> <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>
因而就看到了CAS的登陸界面:
對應的html表單內容大概是:
<form id="fm1" class="fm-v clearfix" action="/cas/login" method="post"> <h2>請輸入您的用戶名和密碼.</h2> <div class="row fl-controls-left"> <label for="username" class="fl-label">用戶名:</label> <input id="username" name="username" class="required" tabindex="1" accesskey="n" type="text" value ="" size="25" autocomplete="false"/> </div> <div class="row fl-controls-left"> <label for="password" class="fl-label">密 碼:</label> <input id="password" name="password" class="required" tabindex="2" accesskey="p" type="password" v alue="" size="25" autocomplete="off"/> </div> <div class="row check"> <input id="warn" name="warn" value="true" tabindex="3" accesskey="w" type="checkbox" /> <label for="warn">轉向其餘站點前提示我。</label> </div> <div class="row btn-row"> <input type="hidden" name="lt" value="LT-5-rCdFkUxqSVKWTpzNgn2hLoZe9Fq0I2" /><!--生成的ticket--> <input type="hidden" name="execution" value="e1s1" /> <input type="hidden" name="_eventId" value="submit" /> <!-- 對應提交到submit事件上--> <input class="btn-submit" name="submit" accesskey="l" value="登陸" tabindex="4" type="submit" /> <input class="btn-reset" name="reset" accesskey="c" value="重置" tabindex="5" type="reset" /> </div> </form>
當咱們點擊「登陸」後,首先就到 authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials), authenticationViaFormAction在cas-servlet.xml中配置:
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction" p:centralAuthenticationService-ref="centralAuthenticationService" p:warnCookieGenerator-ref="warnCookieGenerator"/>
看doBind()方法:
public final void doBind(final RequestContext context, final Credentials credentials) throws Exception { final HttpServletRequest request = WebUtils.getHttpServletRequest(context); // 在authenticationViaFormAction bean定義中並無注入credentialsBinder, 這裏也不會作什麼了 if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) { this.credentialsBinder.bind(request, credentials); } }
接着看submit transition最終流向realSubmit:
<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 { // 首先驗證ticket的一致性 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); } } try { WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials)); //這裏會調用AuthenticationManagerImpl的authenticateAndObtainPricipal方法,該方法會依次調用咱們在deployerConfigContext.xml中配置的authenticationManager bean的authenticationHandlers, 好比以前文章配置的數據庫認證處理器等,驗證成功了就會生成TGT(TicketGrantingTicket)返回給客戶端。 putWarnCookieIfRequestParameterPresent(context); return "success"; } catch (final TicketException e) { populateErrorsInstance(e, messageContext); if (isCauseAuthenticationException(e)) return getAuthenticationExceptionEventId(e); return "error"; } }
假如咱們登陸成功了,flow繼續流向sendTicketGrantingTicket state:
<action-state id="sendTicketGrantingTicket"> <evaluate expression="sendTicketGrantingTicketAction" /> <transition to="serviceCheck" /> </action-state>看看SendTicketGrantingTicketAction作了什麼:
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);//將TGT做爲Cookie加到Response中 if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) { this.centralAuthenticationService .destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie); } return success(); }
返回後,繼續流向serviceCheck state, 會根據service是否爲空來決定怎麼流,也就是說,若是你是直接登陸/cas/login, 那麼就沒有service屬性,若是你是由其餘客戶端跳轉過來登陸的,那麼service就是那個客戶端跳轉登陸的url:
<decision-state id="serviceCheck"> <if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess" /> </decision-state>
若是是直接登陸的cas服務器,登陸成功後,你就能夠看到下面的界面:
<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); //根據TGT生成service ticket WebUtils.putServiceTicketInRequestScope(context, serviceTicketId); //放到request中 return success(); } catch (final TicketException e) { if (isGatewayPresent(context)) { return result("gateway"); } } return error(); }以後,又流向warn state, warnCookieValue就是咱們登陸界面上是否勾選了提示覆選框:
<decision-state id="warn"> <if test="flowScope.warnCookieValue" then="showWarningView" else="redirect" /> </decision-state>
直接看redirect, 其主要構建Response對象,並放到requestScope中:
<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>對於postRedirectDecision state,如果post過來的請求就到視圖就到 /WEB-INF/view/ jsp /protocol/casPostResponseView.jsp ,若get則外部跳轉到會以前的客戶端url
<decision-state id="postRedirectDecision"> <if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView" /> </decision-state>
這就基本說了CAS服務整個登陸怎麼流動,下面也說說,咱們客戶端的處理流程。
-----------------------------------------------------------
web客戶端主要的配置就在web.xml中:
<listener> <listener-class> org.jasig.cas.client.session.SingleSignOutHttpSessionListener </listener-class> </listener> <filter> <filter-name>CasSingleSignOutFilter</filter-name> <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class> </filter> <filter-mapping> <filter-name>CasSingleSignOutFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>CASFilter</filter-name> <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class> <init-param> <param-name>casServerLoginUrl</param-name> <param-value>https://localhost:8443/cas/login</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:8080</param-value> </init-param> </filter> <filter-mapping> <filter-name>CASFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>CasTicketFilter</filter-name> <filter-class> org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>https://localhost:8443/cas</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:8080</param-value> </init-param> </filter> <filter-mapping> <filter-name>CasTicketFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>CasRequestWrapFilter</filter-name> <filter-class> org.jasig.cas.client.util.HttpServletRequestWrapperFilter </filter-class> </filter> <filter-mapping> <filter-name>CasRequestWrapFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>AssertionThreadLocalFilter</filter-name> <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class> </filter> <filter-mapping> <filter-name>AssertionThreadLocalFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>SingleSignOutHttpSessionListener和SingleSignOutFilter用於登出操做。
看CASFilter: 其doFilter方法實現:
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; final HttpSession session = request.getSession(false); final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; if (assertion != null) { //有assertion信息(登陸信息)就經過 filterChain.doFilter(request, response); return; } final String serviceUrl = constructServiceUrl(request, response);//獲取serviceUrl,即當前url final String ticket = CommonUtils.safeGetParameter(request,getArtifactParameterName()); final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { //若是有TGT就表示已登陸過了 filterChain.doFilter(request, response); return; } final String modifiedServiceUrl; if (this.gateway) { log.debug("setting gateway attribute in session"); modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl); } else { modifiedServiceUrl = serviceUrl; } final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); //即將要跳轉到CAS登陸界面的url及其一些參數 response.sendRedirect(urlToRedirectTo); }其中urlToRedirectTo相似:
https://${cas-server-host}:port/cas/login?service=http%3A%2F%2Flocalhost%3A8080%2Fcas-web-client1%2Findex.jsp
通過跳轉,而後登陸成功後的請求信息:
登陸成功之後咱們再訪問須要認證的url時,這時有了TGT, CAS服務端的login-webflow就有變化:
<decision-state id="ticketGrantingTicketExistsCheck"> <if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" /> </decision-state>流向hasServiceCheck state:
<decision-state id="hasServiceCheck"> <if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" /> </decision-state>接着流向renewRequestCheck state:
<decision-state id="renewRequestCheck"> <if test="requestParameters.renew != '' and requestParameters.renew != null" then="serviceAuthorizationCheck" else="generateServiceTicket" /> </decision-state>後面就和以前說的流程同樣了。
當咱們經過redirect返回以前的web客戶端時,還會發生什麼呢,這時有了TGT了,AuthenticationFilter中:
if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { //有TGT經過 filterChain.doFilter(request, response); return; }
因而接着web客戶端下一個的filter Cas20ProxyReceivingTicketValidationFilter:
<filter> <filter-name>CasTicketFilter</filter-name> <filter-class> org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>https://localhost:8443/cas</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:8080</param-value> </init-param> </filter>
Cas20ProxyReceivingTicketValidationFilter過濾處理主要是其父類AbstractTicketValidationFilter實現:
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { //子類預處理,Cas20ProxyReceivingTicketValidationFliter作了一些處理 if (!preFilter(servletRequest, servletResponse, filterChain)) { return; } final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());//獲取ticket if (CommonUtils.isNotBlank(ticket)) { try { final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));//再次拿ticket到服務端驗證,看是否確實存在,或者是否過時, 默認實現爲Cas20ProxyTicketValidator request.setAttribute(CONST_CAS_ASSERTION, assertion); if (this.useSession) {//Aseesion放到session中,因此你就知道怎麼在咱們應用中訪問登陸的用戶信息了 request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion); } onSuccessfulValidation(request, response, assertion); if (this.redirectAfterValidation) { // 默認true log. debug("Redirecting after successful ticket validation."); response.sendRedirect(constructServiceUrl(request, response)); return; } }catch (final TicketValidationException e) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); onFailedValidation(request, response); if (this.exceptionOnValidationFailure) { throw new ServletException(e); } return; } } filterChain.doFilter(request, response); }
validate方法由AbstractBasedTicketValidator實現:
public Assertion validate(final String ticket, final String service) throws TicketValidationException {
//獲取驗證url, 相似https:${cas-server-host}:port/cas/serviceValidate?ticket=xxx&service=yyy final String validationUrl = constructValidationUrl(ticket, service);
if (log.isDebugEnabled()) {
log.debug("Constructing validation url: " + validationUrl);
}
try {
//發送請求並獲取返回內容(經過java URLConnection發送請求,直接讀取Response輸入流)
final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
}
if (log.isDebugEnabled()) {
log.debug("Server response: " + serverResponse);
}
//解析CAS服務端返回的內容爲Assertion對象
return parseResponseFromServer(serverResponse);
} catch (final MalformedURLException e) {
throw new TicketValidationException(e);
}
}
上面發送認證請求後的返回內容相似:
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> <cas:authenticationSuccess> <cas:user>admin</cas:user> </cas:authenticationSuccess> </cas:serviceResponse>
驗證請求/cas/serviceValidate則對應服務器端配置的SafeDispatcherServlet:
這個Servlet中包含有一個咱們熟悉的Spring-MVC的前端分發器DispatcherServlet, 明顯由它來奮發咱們的請求,那麼/validateService對應那個Controller呢?看cas-servlet.xml配置:
看ServiceValidateController的handleRequestInternal方法重要的一句:
final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
就是根據CentralAuthenticationServiceImpl的下面兩個變量來驗證:
/** TicketRegistry for storing and retrieving tickets as needed. */ @NotNull private TicketRegistry ticketRegistry; /** New Ticket Registry for storing and retrieving services tickets. Can point to the same one as the ticketRegistry variable. */ @NotNull private TicketRegistry serviceTicketRegistry;
整個登陸基本流程簡單的瞭解over.