4 源码解析
4.1 Server源码解析
Cas server端采用Spring WebFlow来进行流程控制,因此本文以系统webflow文件为切入点,对流程相关源码进行分析。Cas系统的webflow文件位于WEB-INF/webflow目录下,分为登陆流程和登出流程。
4.1.1 登陆流程解析
4.1.1.1 访问接入Cas系统的应用系统Client1
登陆流程配置文件为login-webflow.xm。
浏览器首次访问配置了单点登录的应用系统时(http://www.client1.com/index),Client1会将请求重定向到cas系统
(http://www.casserver.com/serviceValidate?service=http://www.client1.com/index)
cas系统接收到浏览器发来的请求,整个登录流程从此处开始,流程初始化。
WEB-INF/login-webflow.xml部分代码:
<on-start>
<evaluate expression="initialFlowSetupAction"/>
</on-start>
初始化部分会调用InitialFlowSetupAction类的doExecute方法,如果有特殊需求,可以在此方法中增加相应的逻辑。
InitialFlowSetupAction的doExecute方法:
@Override
protected Event doExecute(final RequestContext context) throws Exception {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
final String contextPath = context.getExternalContext().getContextPath();
final String cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/";
if (StringUtils.isBlank(warnCookieGenerator.getCookiePath())) {
logger.info("Setting path for cookies for warn cookie generator to: {} ", cookiePath);
this.warnCookieGenerator.setCookiePath(cookiePath);
} else {
logger.debug("Warning cookie path is set to {} and path {}", warnCookieGenerator.getCookieDomain(),
warnCookieGenerator.getCookiePath());
}
if (StringUtils.isBlank(ticketGrantingTicketCookieGenerator.getCookiePath())) {
logger.info("Setting path for cookies for TGC cookie generator to: {} ", cookiePath);
this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
} else {
logger.debug("TGC cookie path is set to {} and path {}", ticketGrantingTicketCookieGenerator.getCookieDomain(),
ticketGrantingTicketCookieGenerator.getCookiePath());
}
//将TGT放在FlowScope作用域中
WebUtils.putTicketGrantingTicketInScopes(context,
this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
//将warnCookieValue放在FlowScope作用域中
WebUtils.putWarningCookie(context,
Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
//获取service参数
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);
final RegisteredServiceAccessStrategy accessStrategy = registeredService.getAccessStrategy();
if (accessStrategy.getUnauthorizedRedirectUrl() != null) {
logger.debug("Placing registered service's unauthorized redirect url [{}] with id [{}]in context scope",
accessStrategy.getUnauthorizedRedirectUrl(),
registeredService.getServiceId());
WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl());
}
}
} 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");
}
InitialFlowSetupAction的doExecute要做的就是把ticketGrantingTicketId,warnCookieValue和service放到FlowScope的作用域中,以便在登录流程中的state中进行判断。初始化完成后,登录流程流转到第一个state(ticketGrantingTicketExistsCheck)。
<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方法判断request的Cookie中是否携带有效的TGT,第一次访问时没有携带TGT,流程跳转到gatewayRequestCheck。
<decision-state id="gatewayRequestCheck">
<if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null"
then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck"/>
</decision-state>
因为初始化时,尽管把service保存在了FlowScope作用域中,但request中的参数gateway不存在,登录流程流转到第三个state(serviceAuthorizationCheck)。
<action-state id="serviceAuthorizationCheck">
<evaluate expression="serviceAuthorizationCheck"/>
<transition to="initializeLogin"/>
</action-state>
ServiceAuthorizationCheck的doExecute方法,要做的就是判断FlowScope作用域中是否存在service,如果service存在,查找service的注册信息。登录流程流转到第四个state(generateLoginTicket)。
<action-state id="initializeLogin">
<evaluate expression="'success'"/>
<transition on="success" to="viewLoginForm"/>
</action-state>
initializeLogin不做判断,存在只是为了兼容旧cas版本。直接跳转到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>
此时流转到CAS单点登录服务器端的登录页面casLoginView.jsp。
<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="initializeLogin"/>
</action-state>
用户在登录页面输入账号密码提交后,流程走到realSumit。
authenticationViaFormAction类的submit()对用户提交的认证信息进行验证。
public final Event submit(final RequestContext context, final Credential credential,
final MessageContext messageContext) {
//判断是否是已登录过,请求ST的
if (isRequestAskingForServiceTicket(context)) {
//如果已登录,则生成ST
return grantServiceTicket(context, credential);
}
//未登陆过,生成TGT
return createTicketGrantingTicket(context, credential, messageContext);
}
验证成功则跳转到sendTicketGrantingTicket。
<action-state id="sendTicketGrantingTicket">
<evaluate expression="sendTicketGrantingTicketAction"/>
<transition to="serviceCheck"/>
</action-state>
接着跳转到serviceCheck
<decision-state id="serviceCheck">
<if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess"/>
</decision-state>
判断是否是由应用页面跳转到登录页面登陆的,如果是,则跳转到generateServiceTicket,不是则跳转到viewGenericLoginSuccess。
此处我们跳转到generateServiceTicket
<action-state id="generateServiceTicket">
<evaluate expression="generateServiceTicketAction"/>
<transition on="success" to="warn"/>
<transition on="unregisteredService" to="viewGenericLoginSuccess"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="initializeLogin"/>
<transition on="gateway" to="gatewayServicesManagementCheck"/>
</action-state>
generateServiceTicketAction类的doExecute方法生成ST,并跳转到warn
<decision-state id="warn">
<if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/>
</decision-state>
跳转到redirect
<action-state id="redirect">
<evaluate expression="flowScope.service.getResponse(requestScope.serviceTicketId)"
result-type="org.jasig.cas.authentication.principal.Response" result="requestScope.response"/>
<transition to="postRedirectDecision"/>
</action-state>
<decision-state id="postRedirectDecision">
<if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView"/>
</decision-state>
<end-state id="redirectView" view="externalRedirect:#{requestScope.response.url}"/>
最终返回给浏览器跳转回Client1的响应。
4.1.1.2 访问接入Cas系统的应用系统Client2
访问Client1并登陆之后,访问Client2,与访问Client1一样,先经过initialFlowSetupAction。
随后登录流程流转到第一个state(ticketGrantingTicketExistsCheck)。
<action-state id="ticketGrantingTicketCheck">
<evaluate expression="ticketGrantingTicketCheckAction"/>
<transition on="notExists" to="gatewayRequestCheck"/>
<transition on="invalid" to="terminateSession"/>
<transition on="valid" to="hasServiceCheck"/>
</action-state>
因为已经登陆过,拥有请求的Cookie中存在有效的TGT,于是流程跳转到hasServiceCheck。
<decision-state id="hasServiceCheck">
<if test="flowScope.service != null" then="renewRequestCheck" else="viewGenericLoginSuccess"/>
</decision-state>
判断是否是由应用页面跳转到登录页面登陆的,如果是,则跳转到generateServiceTicket,不是则跳转到viewGenericLoginSuccess。
此处跳转到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="unregisteredService" to="viewGenericLoginSuccess"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="initializeLogin"/>
<transition on="gateway" to="gatewayServicesManagementCheck"/>
</action-state>
后续的流转与应用系统webapp1相同,请参考前面webapp1的流转。
4.1.2 登出流程解析
登出的流程定义在logout-webflow.xml中。
首先访问登出接口/logout,流程跳转到terminateSession
<action-state id="terminateSession">
<evaluate expression="terminateSessionAction.terminate(flowRequestContext)" />
<transition to="doLogout" />
</action-state>
登出的方法主要调用路径如下:
TerminateSessionAction.terminate()
--> CentralAuthenticationServiceImpl.destroyTicketGrantingTicket()
销毁TGT的方法
--> LogoutManagerImpl.performLogout()
执行登出的方法,在该方法中向每个访问过的应用系统发送登出请求, 应用系统收到请求会销毁与用户的session
--> handleLogoutForSloService()
向应用系统发送登出请求的方法
--> performBackChannelLogout() 发送登出请求
terminateSessionAction.terminate()执行完毕之后,流程跳转到doLogout
<action-state id="doLogout">
<evaluate expression="logoutAction" />
<transition on="finish" to="finishLogout" />
<transition on="front" to="frontLogout" />
</action-state>
<decision-state id="finishLogout">
<if test="flowScope.logoutRedirectUrl != null" then="redirectView" else="logoutView" />
</decision-state>
<end-state id="logoutView" view="externalRedirect:casLoginView" />
最终跳转到登录页面。
4.2 Client端源码解析
Cas client应用系统端通过几个Filter来实现登陆跳转和登出等功能。
下面以在web.xml中配置的几个Filter顺序来进行分析 。
4.2.1 SingleSignOutFilter
org.jasig.cas.client.session.SingleSignOutFilter是处理登出请求的Filter。该Filter判断是否是Cas Server端发过来的登出请求,如果是登出请求,则根据请求中的logoutMessage清除对应的Session。
登出请求的主要调用路径如下:
SingleSignOutFilter.doFilter()
--> SingleSignOutHandler.process()
--> destroySession(request)
--> session.invalidate();
4.2.2 AuthenticationFilter
org.jasig.cas.client.authentication.AuthenticationFilter是验证请求是否登陆过的Filter。
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
// 判断该请求是否不需要验证,如果不需要,则跳转到下一个Filter
if(this.isRequestUrlExcluded(request)) {
this.logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
} else {
HttpSession session = request.getSession(false);
//从session中获取名为"_const_cas_assertion_"的Assertion
Assertion assertion = session != null?(Assertion)session.getAttribute("_const_cas_assertion_"):null;
//如果存在,则说明已经登录,本过滤器处理完成,处理下个过滤器
if(assertion != null) {
filterChain.doFilter(request, response);
} else {
//生成serviceUrl
String serviceUrl = this.constructServiceUrl(request, response);
//从request中获取ST
String ticket = this.retrieveTicketFromRequest(request);
boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
//如果ticket不为空,本过滤器处理完成,处理下个过滤器
if(!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
this.logger.debug("no ticket and no assertion found");
String modifiedServiceUrl;
if(this.gateway) {
this.logger.debug("setting gateway attribute in session");
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
//生成重定向URL
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
String reqType = request.getHeader("X-Requested-With");
//如果是异步请求,则返回410状态码给前端
if("XMLHttpRequest".equalsIgnoreCase(reqType)) {
String json = "{\"flag\":0,\"error\":401,\"data\":{}}";
response.getWriter().write(json);
} else {
//跳转到CAS服务器的登录页面
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
当我们从浏览器访问配置了单点登录的应用系统时(http://www.client1.com/index),由于集成了CAS单点登录客户端,此时进入到第一个过滤器AuthenticationFilter(不考虑其他非单点登录的过滤器),执行以下操作:
1 从session中获取名为“const_cas_assertion”的assertion对象,判断assertion是否存在,如果存在,说明已经登录,执行下一个过滤器。如果不存在,执行第2步。
2 生成serviceUrl(http://www.client1.com/index),从request中获取票据参数ticket,判断ticket是否为空,如果不为空执行下一个过滤器。如果为空,执行第3步。
3 生成重定向URL,如:
http://www.casserver.com/login?service=http://www.client1.com/index
4 跳转到单点登录服务器,显示登录页面,此时第一个过滤器执行完成。
4.2.3 ticketValidationFilter
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter为验证Service Ticket的Filter。
Cas20ProxyReceivingTicketValidationFilter父类AbstractTicketValidationFilter中的doFilter方法:
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if(this.preFilter(servletRequest, servletResponse, filterChain)) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//从request中获取参数
String ticket = this.retrieveTicketFromRequest(request);
//ticket不为空,验证ticket,否则本过滤器处理完成,处理下个过滤器
if(CommonUtils.isNotBlank(ticket)) {
this.logger.debug("Attempting to validate ticket: {}", ticket);
try {
//验证ticket并产生Assertion对象,错误抛出TicketValidationException异常
Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
this.logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
//给request设置assertion
request.setAttribute("_const_cas_assertion_", assertion);
//给session设置assertion
if(this.useSession) {
request.getSession().setAttribute("_const_cas_assertion_", assertion);
}
this.onSuccessfulValidation(request, response, assertion);
if(this.redirectAfterValidation) {
this.logger.debug("Redirecting after successful ticket validation.");
response.sendRedirect(this.constructServiceUrl(request, response));
return;
}
} catch (TicketValidationException var8) {
this.logger.debug(var8.getMessage(), var8);
this.onFailedValidation(request, response);
if(this.exceptionOnValidationFailure) {
throw new ServletException(var8);
}
response.sendError(403, var8.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
}
假设当执行完第一个过滤器后,跳转到CAS服务器端的登录页面,输入用户名和密码,验证通过后。CAS服务器端会生成ticket,并将ticket作为重新跳转到应用系统的参数(http://www.client1.com/index?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)。此时又进入第一个过滤器AuthenticationFilter,由于存在ticket参数,进入到第二个过滤器TicketValidationFilter,执行以下操作:
1 从request获取ticket参数,如果ticket为空,继续处理下一个过滤器。如果参数不为空,验证ticket参数的合法性。
2 验证ticket,TicketValidationFilter的validate方法通过httpClient访问CAS服务器端(http://www.casserver.com/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://www.client1.com/index)验证ticket是否正确,并返回assertion对象。如果验证失败,抛出异常,跳转到错误页面。如果验证成功,session会以"const_cas_assertion"的名称保存assertion对象,继续处理下一个过滤器。
4.2.4 HttpServletRequestWrapperFilter
org.jasig.cas.client.util.HttpServletRequestWrapperFilter对HttpServletRequest对象再包装一次,让其支持getUserPrincipal,getRemoteUser方法来取得登录的用户信息。
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//从Session或者request中取得AttributePrincipal,其实Assertion的一个principal属性
AttributePrincipal principal = this.retrievePrincipalFromSessionOrRequest(servletRequest);
//对request进行包装,并处理后面的过滤器,使其后面的过滤器或者servlet能够在request.getRemoteUser()或者request.getUserPrincipal()取得用户信息
filterChain.doFilter(newHttpServletRequestWrapperFilter.CasHttpServletRequestWrapper((HttpServletRequest)servletRequest, principal), servletResponse); }
5 常见问题
6 其他
6.1 Gradle相关
6.1.1 Gradle 打包实现生产环境与测试环境配置分离
在build.gradle中加入以下代码
#默认情况下为ent-dev
def env = System.getProperty("profile") ?: "ent-dev"
sourceSets {
main {
resources {
srcDirs = ["src/main/resources", "src/main/$env"]
}
}
}
在/src/main目录下建立各个环境目录,如 ent-dev、ent-prod等。
对于cas系统,配置参数都在cas.properties中,可以将cas.properties放入各个环境目录中, 并修改读取cas.properties路径。
修改WEB-INF/spring-configuration目录中的propertyFileConfigurer.xml
将
<util:properties id="casProperties" location="${cas.properties.config.location:/WEB-INF/cas.properties}"/>
修改为
<util:properties id="casProperties" location="${cas.properties.config.location:classpath:/cas.properties}"/>
build生产环境时可使用命令:gradle build -D profile=ent-prod
6.2 调用263和LDAP验证用户名密码步骤
6.2.1 在项目中添加依赖
在项目build.gradle中加入以下内容,如果是Maven项目,则在pom.xml文件中加入maven格式依赖。
shangdeCommonSdfVersion=0.1.0.5-ENT-SNAPSHOT
compile group: 'com.xxx.common', name: 'sdf-common-util', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-web', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-auth-web', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-sys', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-authentication-263', version: shangdeCommonSdfVersion
compile group: 'com.xxx.common', name: 'sdf-common-auth', version: shangdeCommonSdfVersion
6.2.2 在properties文件中添加以下内容
263.domain=xyz.com
263.account=xyz.com
263.key=Zs54D6jo#
263.webServiceUrl=http://macom.263.net/axis/xmapi
ldap.url=ldap://172.16.117.215:389
6.2.3 调用验证方法
@Autowired
private SdfPasswordValidatorImpl sdfPasswordValidatorImpl;
// 返回值为true及验证成功
boolean isValidUser = sdfPasswordValidatorImpl.authenticate(username,password);
作者:Ferrari1001
链接:https://www.jianshu.com/p/3dcaaa12e976
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。