jasig CAS登录验证分析:
之前文章讲到了怎么利用jasig CAS实现sso:
http://my.oschina.net/indestiny/blog/200768
本文对jasig CAS验证过程做个简单的分析,便于以后能够更好定制自己的CAS, 要了解CAS流程你需要知道spring,springmvc等知识,也要了解spring-webflow, 因为整个验证流程都是由spring-webflow定制的,你可以参考我转载的一篇spring-webflow的文章:
http://my.oschina.net/indestiny/blog/201988
ok, 就开始了。
- 先说说我们未登录状态时:
重点就是服务器端的配置: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中:
<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:
<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服务器,登录成功后,你就可以看到下面的界面:
我们假设是从你的另一个web client跳转过来的,那么就会流向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); //根据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.
来源:oschina
链接:https://my.oschina.net/u/222173/blog/202454