CAS服务端流程分析
'CAS单点登录服务器端的登录流程'
-----流程的配置在/WEB-INF/login-webflow.xml文件中
<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>
-----首先,设置一个变量,用来存储用户名和密码信息
<on-start>
<evaluate expression="initialFlowSetupAction"/>
</on-start>
-------从此开始整个登录流程,initialFlowSetupAction,的配置信息/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"
p:servicesManager-ref="servicesManager"
p:enableFlowOnAbsentServiceRequest="${create.sso.missing.service:true}" />
---------'argumentExtractors'
其中'argumentExtractors'配置文件在/WEB-INF/spring-configuration/argumentExtractorsConfiguration.xml
<bean id="casArgumentExtractor" class="org.jasig.cas.web.support.CasArgumentExtractor"/>
<util:list id="argumentExtractors">
<!-- <ref bean="samlArgumentExtractor" /> -->
<ref bean="casArgumentExtractor"/>
</util:list>
------'warnCookieGenerator'
其中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"/>
------'ticketGrantingTicketCookieGenerator'
其中ticketGrantingTicketCookieGenerator的配置文件在/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml
<bean id="ticketGrantingTicketCookieGenerator"
class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
c:casCookieValueManager-ref="cookieValueManager"
p:cookieSecure="false"
p:cookieMaxAge="-1"
p:cookieName="TGC"
p:cookiePath="/cas"/>
初始化部分会调用initialFlowSetupAction的doExecute方法,如果有特殊需求,可以在此方法中增加相应的逻辑。
-->InitialFlowSetupAction--doExecute
/**
* doExecute的目的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域
* 以便在登陆流程中的state进行判断,初始化完成后,登陆流程转到第一个state-->ticketGrantingTicketExistsCheck
*/
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;
}
//获取并设置ticketGrantingTicketId,即TGT,用于证明用户已经登录
WebUtils.putTicketGrantingTicketInScopes(context, this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
//将warnCookieValue放在FlowScope中
WebUtils.putWarningCookie(context, Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
//获取service参数,即用户重定向/cas/login之前访问的URL
final Service service = WebUtils.getService(this.argumentExtractors, context);
if (service != null) {
logger.debug("Placing service in context scope: [{}]", service.getId());
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) {
logger.debug("Placing registered service [{}] with id [{}] in context scope",
registeredService.getServiceId(),
registeredService.getId());
WebUtils.putRegisteredService(context, registeredService);
}
} else if (!this.enableFlowOnAbsentServiceRequest) {
logger.warn("No service authentication request is available at [{}]. CAS is configured to disable the flow.",
WebUtils.getHttpServletRequest(context).getRequestURL());
throw new NoSuchFlowExecutionException(context.getFlowExecutionContext().getKey(),
new UnauthorizedServiceException("screen.service.required.message", "Service is required"));
}
//将service放在FlowScope的作用域中
WebUtils.putService(context, service);
//成功初始化,进入下一个流程
return result("success");
}
----初始化完成后,登陆流程转到第一个state,ticketGrantingTicketCheck /WEB-INF/webflow/login/login-webfolw.xml
<action-state id="ticketGrantingTicketCheck">
<evaluate expression="ticketGrantingTicketCheckAction"/>
<transition on="notExists" to="gatewayRequestCheck"/>
<transition on="invalid" to="terminateSession"/>
<transition on="valid" to="hasServiceCheck"/>
</action-state>
--->TicketGrantingTicketCheckAction-->doExecute
@Override
protected Event doExecute(final RequestContext requestContext) throws Exception {
final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
if (!StringUtils.hasText(tgtId)) {
return new Event(this, NOT_EXISTS);
}
String eventId = INVALID;
try {
final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
if (ticket != null && !ticket.isExpired()) {
eventId = VALID;
}
} catch (final TicketException e) {
logger.trace("Could not retrieve ticket id {} from registry.", e);
}
return new Event(this, eventId);
}
当我们第一次访问集成了CAS单点登录的应用系统WEBAPP1时(http://127.0.0.1:8090/webapp1/main.do) 此时应系统会跳转到CAS的单点登录
的服务器端(http://127.0.0.1:/8090/cas-server/login?service=http://127.0.0.1:8090/webapp1/main.do)
由于此时request的cookie中不存在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不存在,登录流程流转到第三个节点serviceAuthorizationCheck
----登录流转到serviceAuthorizationCheck阶段
//这步的目的就是,在登陆前做一个服务授权检查
<action-state id="serviceAuthorizationCheck">
<evaluate expression="serviceAuthorizationCheck"/>
<transition to="generateLoginTicket"/>
</action-state>
--->ServiceAuthorizationCheck-->doExecte--
/** 目的就是判断FlowScope作用域是否存在service,如果service存在,查找service的注册信息。登录流程转到第四个state(generateLoginTicket) */
@Override
protected Event doExecute(final RequestContext context) throws Exception {
final Service service = WebUtils.getService(context);
//No service == plain /login request. Return success indicating transition to the login form
if (service == null) { return success(); }
if (this.servicesManager.getAllServices().isEmpty()) {
final String msg = String.format("No service definitions are found in the service manager. "
+ "Service [%s] will not be automatically authorized to request authentication.", service.getId());
logger.warn(msg);
throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_EMPTY_SVC_MGMR);
}
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
if (registeredService == null) {
final String msg = String.format("ServiceManagement: Unauthorized Service Access. "
+ "Service [%s] is not found in service registry.", service.getId());
logger.warn(msg);
throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
}
if (!registeredService.getAccessStrategy().isServiceAccessAllowed()) {
final String msg = String.format("ServiceManagement: Unauthorized Service Access. "
+ "Service [%s] is not enabled in service registry.", service.getId());
logger.warn(msg);
throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
}
return success();
}
---登录流转到:generateLoginTicket
<action-state id="generateLoginTicket">
<evaluate expression="generateLoginTicketAction.generate(flowRequestContext)"/>
<transition on="generated" to="viewLoginForm"/>
</action-state>
'-----generateLoginTicket---/WEB-INF/cas-servlet.xml部分代码------'
<bean id="generateLoginTicketAction"
class="org.jasig.cas.web.flow.GenerateLoginTicketAction"
p:ticketIdGenerator-ref="loginTicketUniqueIdGenerator"/>
'-----loginTicketUniqueIdGenerator---/WEB-INF/spring-configuration/uniquieIdGenerator.xml---'
<bean id="loginTicketUniqueIdGenerator"
class="org.jasig.cas.util.DefaultUniqueTicketIdGenerator"
c:maxLength="30"
c:suffix="${host.name}" />
DefaultUniqueTicketIdGenerator的作用就是生成以LT作为前缀的loginTicket(例如:LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn),LT只作为登录使用的票据。
//--->GenerateLoginTicketAction的generate方法,该方法的主要目的就是loginTicket放到FlowScope作用域中
public final String generate(final RequestContext context) {
//通过DefaultUniqueTicketGenerator生成loginTicket,可以通过实现实现UniqueTicketIdGenerator.java,来自定义生成loginTicket的格式
final String loginTicket = this.ticketIdGenerator.getNewTicketId(PREFIX); //这里ticketIdGerator就是DefaultUniqueTicketIdGenerator
logger.debug("Generated login ticket {}", loginTicket);
//ticket放入FlowScope
WebUtils.putLoginTicket(context, loginTicket);
return "generated";
}
-----登录流转到: viewLoginForm
<view-state id="viewLoginForm" view="casLoginView" model="credential">
<binder>
<binding property="username" required="true"/>
<binding property="password" required="true"/>
</binder>
<on-entry>
<set name="viewScope.commandName" value="'credential'"/>
</on-entry>
<transition on="submit" bind="true" validate="true" to="realSubmit"/>
</view-state>
到此,经过5个state 的流转,就完成了第一次访问集成单点登录的应用系统,此时流转到CAS单点登录服务器的登录页面/WEB-INF/jsp/ui/default/casLoginView.jsp
由于casLoginView.jsp是CAS提供的默认登录页面,需要把此页面修改为我们系统需要的登录页面,格式需要参考casLoginView.jsp;
注意,默认的登录页面中有lt,execution和_eventId三个隐藏参数,It参数值就是GenerateLoginTicketAction的generate方法中生成的loginTicket.
-----casLoginView.jsp
<input type="hidden" name="lt" value="${loginTicket}" />
<input type="hidden" name="execution" value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
cas 服务端的登录认证
当我们在登录页面输入用户名和密码,点击登录后会执行AuthenticationViaFormAction的doBind方法
---配置文件/WEB-INF/cas-servlet.xml----
<bean id="authenticationViaFormAction"
class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
p:centralAuthenticationService-ref="centralAuthenticationService"
p:warnCookieGenerator-ref="warnCookieGenerator"/>
--------流程跳转到---------------
<action-state id="realSubmit">
<evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)"/>
<transition on="warn" to="warn"/>
<transition on="success" to="sendTicketGrantingTicket"/>
<transition on="successWithWarnings" to="showMessages"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="generateLoginTicket"/>
</action-state>
---->AuthenticationViaFormAction的---submit方法----
public final Event submit(final RequestContext context, final Credential credential,
final MessageContext messageContext) {
//判断FlowScope和request域中的loginTicket是否相同
if (!checkLoginTicketIfExists(context)) {
return returnInvalidLoginTicketEvent(context, messageContext);
}
//根据用户凭证生成TGT(登录成功票据),并放到requestScope作用域中,同时把TGT缓存到服务器cache<ticketId,TGT>中
if (isRequestAskingForServiceTicket(context)) {
return grantServiceTicket(context, credential);
}
return createTicketGrantingTicket(context, credential, messageContext);
}
----登录流程跳转到第二个state-----
<action-state id="sendTicketGrantingTicket">
<evaluate expression="sendTicketGrantingTicketAction"/>
<transition to="serviceCheck"/>
</action-state>
--->SendTicketGrantingTicketAction的doExecute的方法
/*** SendTicketGrantingTicketAction的要做的获取TGT,并根据TGT生成cookie添加到response*/
protected Event doExecute(final RequestContext context) {
//获取requestScope和FlowScope中的TGT
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");
if (ticketGrantingTicketId == null) { return success(); }
if (isAuthenticatingAtPublicWorkstation(context)) {
LOGGER.info("Authentication is at a public workstation. "
+ "SSO cookie will not be generated. Subsequent requests will be challenged for authentication.");
} else if (!this.createSsoSessionCookieOnRenewAuthentications && isAuthenticationRenewed(context)) {
LOGGER.info("Authentication session is renewed but CAS is not configured to create the SSO session. "
+ "SSO cookie will not be generated. Subsequent requests will be challenged for authentication.");
} else {
//response中添加TGC
this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils
.getHttpServletResponse(context), ticketGrantingTicketId);
}
if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
}
return success();
}
------登录流程跳转到第三个state
<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
<action-state id="generateServiceTicket">
<evaluate expression="generateServiceTicketAction"/>
<transition on="success" to="warn"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="generateLoginTicket"/>
<transition on="gateway" to="gatewayServicesManagementCheck"/>
</action-state>
/**
* GenerateServiceTicketAction的doExecute要做的是获取service和TGT
* 并根据service和TGT生成以ST为前缀的serviceTicket,并把serviceTicket放到requestScope中
* */
@Override
protected Event doExecute(final RequestContext context) {
//获取service
final Service service = WebUtils.getService(context);
//获取TGT
final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
try {
final Credential credential = WebUtils.getCredential(context);
//根据TGT和service ticket(ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org)
final ServiceTicket serviceTicketId = this.centralAuthenticationService
.grantServiceTicket(ticketGrantingTicket, service, credential);
//ST放到requestScope中
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
return success();
} catch (final AuthenticationException e) {
logger.error("Could not verify credentials to grant service ticket", e);
} catch (final TicketException e) {
if (e instanceof InvalidTicketException) {
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
}
if (isGatewayPresent(context)) {
return result("gateway");
}
}
return error();
}
------登录流程跳转到第五个state
<decision-state id="warn">
<if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/>
</decision-state>
由于此时FlowScope中不存在warnCookieValue,所以跳转到redirect
------登录流程跳转到第六个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>
在这一步从reuqestScope中获取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?server=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_"的assert对象不存在,但request有ticket,所以进入下一个过滤器
> TicketValidationFilter过滤器的validate方法通过httpclient访问CAS服务器
http://127.0.0.1:8081/cas-server/serviceValidate?ticket=ST-1-4hHxxxxcas01.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)
InitialFlowStepAction的doExecutor初始化完成后,登录流程流转到第一个state
------登录流程跳转到第一个state:ticketGrantingTicketExistsCheck
<decision-state id="ticketGrantingTicketExistsCheck">
<if test="flowScope.ticketGrantingTicketId != null" then="hasServiceCheck" else="gatewayRequestCheck" />
</decision-state>
因为应用系统webapp1已经成功登录,所以request的cookies中存在TGT,并保存到FlowScope中登录流转到第二个state
------登录流转到第二个state:hasServiceCheck
<decision-state id="hasServiceCheck">
<if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" />
</decision-state>
FlowScope中存在service,登录流转到第三个state(renewRequestCheck)
------登录流转到第二个state:hasServiceCheck
<decision-state id="hasServiceCheck">
<if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess" />
</decision-state>
------登录流转到第三个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相同---