以前文章讲到了怎么利用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.