相关阅读
login请求的处理流程配置
/WEB-INF/web.xml
中欢迎页的配置如下:
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
index.jsp的内容如下:
<%@ page language="java" session="false" %>
<%
final String queryString = request.getQueryString();
final String url = request.getContextPath() + "/login" + (queryString != null ? '?' + queryString : "");
response.sendRedirect(response.encodeURL(url));%>
将访问请求重定向至http://ip:port/cas/login?queryString
;
在/WEB-INF/web.xml
中定义了拦截处理login请求的servlet,配置如下:
<servlet>
<servlet-name>cas</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!-- Load the child application context. Start with the default, then modules, then overlays. -->
<param-value>/WEB-INF/cas-servlet.xml,classpath*:/META-INF/cas-servlet-*.xml,/WEB-INF/cas-servlet-*.xml</param-value>
</init-param>
<init-param>
<param-name>publishContext</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>cas</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
...
/WEB-INF/web.xml
会引入/WEB-INF/cas-servlet.xml
,该文件定义了login请求的映射逻辑,相关配置如下:
<!-- cas-servlet.xml -->
<!-- login webflow configuration -->
<bean id="loginFlowHandlerMapping" class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping"
p:flowRegistry-ref="loginFlowRegistry" p:order="2">
<property name="interceptors">
<array value-type="org.springframework.web.servlet.HandlerInterceptor">
<ref bean="localeChangeInterceptor"/>
<ref bean="authenticationThrottle"/>
</array>
</property>
</bean>
<bean name="loginFlowExecutor" class="org.springframework.webflow.executor.FlowExecutorImpl"
c:definitionLocator-ref="loginFlowRegistry"
c:executionFactory-ref="loginFlowExecutionFactory"
c:executionRepository-ref="loginFlowExecutionRepository"/>
<bean name="loginFlowExecutionFactory" class="org.springframework.webflow.engine.impl.FlowExecutionImplFactory"
p:executionKeyFactory-ref="loginFlowExecutionRepository"/>
<bean id="loginFlowExecutionRepository" class=" org.jasig.spring.webflow.plugin.ClientFlowExecutionRepository"
c:flowExecutionFactory-ref="loginFlowExecutionFactory"
c:flowDefinitionLocator-ref="loginFlowRegistry"
c:transcoder-ref="loginFlowStateTranscoder"/>
<bean id="loginFlowStateTranscoder" class="org.jasig.spring.webflow.plugin.EncryptedTranscoder"
c:cipherBean-ref="loginFlowCipherBean" />
loginFlowRegistry
配置在/WEB-INF/spring-configuration/webflowContext.xml
中,该配置文件由/WEB-INF/web.xml
文件引入,具体如下:
<!-- web.xml -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring-configuration/*.xml
/WEB-INF/deployerConfigContext.xml
<!-- this enables extensions and addons to contribute to overall CAS' application context
by loading spring context files from classpath i.e. found in classpath jars, etc. -->
classpath*:/META-INF/spring/*.xml
</param-value>
</context-param>
配置文件/WEB-INF/spring-configuration/webflowContext.xml
中loginFlowRegistry
的配置如下:
<!-- webflowContext.xml -->
<webflow:flow-registry id="loginFlowRegistry" flow-builder-services="builder" base-path="/WEB-INF/webflow">
<webflow:flow-location-pattern value="/login/*-webflow.xml"/>
</webflow:flow-registry>
定义了流程配置文件路径:/WEB-INF/webflow/login/*-webflow.xml
,即/WEB-INF/webflow/login/login-webflow.xml
,该文件定义了login请求的处理流程;
login请求的处理流程开启
根据/WEB-INF/cas-servlet.xml
的配置:
<bean id="loginHandlerAdapter" class="org.jasig.cas.web.flow.SelectiveFlowHandlerAdapter"
p:supportedFlowId="login" p:flowExecutor-ref="loginFlowExecutor" p:flowUrlHandler-ref="loginFlowUrlHandler"/>
...
<bean name="loginFlowExecutor" class="org.springframework.webflow.executor.FlowExecutorImpl"
c:definitionLocator-ref="loginFlowRegistry"
c:executionFactory-ref="loginFlowExecutionFactory"
c:executionRepository-ref="loginFlowExecutionRepository"/>
由p:supportedFlowId="login"
可知,请求http://ip:port/cas/login?queryString
会被loginHandlerAdapter
处理,其定义的loginFlowExecutor
属性会引用到loginFlowRegistry
,即/WEB-INF/webflow/login/login-webflow.xml
文件中配置的处理流程;
先分析SelectiveFlowHandlerAdapter
的处理,其核心逻辑继承自父类FlowHandlerAdapter
,代码如下:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
FlowHandler flowHandler = (FlowHandler)handler;
this.checkAndPrepare(request, response, false);
// 根据cas-servlet.xml配置<bean id="loginFlowUrlHandler" class="org.jasig.cas.web.flow.CasDefaultFlowUrlHandler"/>
// flowUrlHandler为CasDefaultFlowUrlHandler
// getFlowExecutionKey(request)的实现为:request.getParameter("execution")
String flowExecutionKey = this.flowUrlHandler.getFlowExecutionKey(request);
// 第一次请求,request的"execution"属性为null
if (flowExecutionKey != null) {
try {
ServletExternalContext context = this.createServletExternalContext(request, response);
FlowExecutionResult result = this.flowExecutor.resumeExecution(flowExecutionKey, context);
this.handleFlowExecutionResult(result, context, request, response, flowHandler);
} catch (FlowException var11) {
this.handleFlowException(var11, request, response, flowHandler);
}
} else {
try {
// flowId即"login"
String flowId = this.getFlowId(flowHandler, request);
MutableAttributeMap<Object> input = this.getInputMap(flowHandler, request);
ServletExternalContext context = this.createServletExternalContext(request, response);
// 启动登录流程处理
FlowExecutionResult result = this.flowExecutor.launchExecution(flowId, input, context);
this.handleFlowExecutionResult(result, context, request, response, flowHandler);
} catch (FlowException var10) {
this.handleFlowException(var10, request, response, flowHandler);
}
}
return null;
}
其中this.flowExecutor.launchExecution
即FlowExecutorImpl.launchExecution
,代码如下:
public FlowExecutionResult launchExecution(String flowId, MutableAttributeMap<?> input, ExternalContext context) throws FlowException {
FlowExecutionResult var6;
try {
if (logger.isDebugEnabled()) {
logger.debug("Launching new execution of flow '" + flowId + "' with input " + input);
}
ExternalContextHolder.setExternalContext(context);
// 根据loginFlowRegistry加载流程定义
FlowDefinition flowDefinition = this.definitionLocator.getFlowDefinition(flowId);
// 根据loginFlowExecutionFactory创建流程
FlowExecution flowExecution = this.executionFactory.createFlowExecution(flowDefinition);
// 开启流程
flowExecution.start(input, context);
if (!flowExecution.hasEnded()) {
FlowExecutionLock lock = this.executionRepository.getLock(flowExecution.getKey());
lock.lock();
try {
this.executionRepository.putFlowExecution(flowExecution);
} finally {
lock.unlock();
}
FlowExecutionResult var7 = this.createPausedResult(flowExecution);
return var7;
}
var6 = this.createEndResult(flowExecution);
} finally {
ExternalContextHolder.setExternalContext((ExternalContext)null);
}
return var6;
}
login请求的处理流程简析
1 on-start
login请求的处理流程定义在/WEB-INF/webflow/login/login-webflow.xml
文件中;
先是执行<on-start>
动作,配置如下:
<on-start>
<evaluate expression="initialFlowSetupAction"/>
</on-start>
initialFlowSetupAction
对应的是InitialFlowSetupAction
,其父类AbstractAction
定义了执行的算法模板,代码如下:
public final Event execute(RequestContext context) throws Exception {
Event result = this.doPreExecute(context);
if (result == null) {
// 启动处理流程
result = this.doExecute(context);
this.doPostExecute(context);
} else if (this.logger.isInfoEnabled()) {
this.logger.info("Action execution disallowed; pre-execution result is '" + result.getId() + "'");
}
return result;
}
InitialFlowSetupAction
实现了算法细节doExecute
,代码如下:
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 + '/' : "/";
// 设置cookie路径
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());
}
// 保存cookie中的TGT信息
WebUtils.putTicketGrantingTicketInScopes(context,
this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
// 保存cookie中的WARNING信息
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());
// 根据请求携带的Service信息查找已注册的Service信息
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());
// 保存已注册Service信息
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());
// 保存重定向URL信息
WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl());
}
}
} else if (!this.enableFlowOnAbsentServiceRequest) {
// 不支持空Service请求
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信息
WebUtils.putService(context, service);
return result("success");
}
2 action-state
因为没有start-state
,则根据流程配置执行第一个action-state
,配置如下:
<!-- login-webflow.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>
ticketGrantingTicketCheck
对应的是TicketGrantingTicketCheckAction
,继承自AbstractAction
,实现了算法细节doExecute
,代码如下:
protected Event doExecute(final RequestContext requestContext) throws Exception {
// 从上下文中获取TGT信息
final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
if (!StringUtils.hasText(tgtId)) {
// 不存在,则返回NOT_EXISTS事件
return new Event(this, NOT_EXISTS);
}
// 默认TGT无效
String eventId = INVALID;
try {
// 获取ticket信息
final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
if (ticket != null && !ticket.isExpired()) {
// TGT有效
eventId = VALID;
}
} catch (final AbstractTicketException e) {
logger.trace("Could not retrieve ticket id {} from registry.", e);
}
return new Event(this, eventId);
}
login请求时,cookie中不存在TGT信息,所以走notExists
处理,即gatewayRequestCheck
,其配置如下:
<!-- login-webflow.xml -->
<decision-state id="gatewayRequestCheck">
<if test="requestParameters.gateway != '' and requestParameters.gateway != null and flowScope.service != null"
then="gatewayServicesManagementCheck" else="serviceAuthorizationCheck"/>
</decision-state>
根据表达式可知,未携带Service
信息的login请求,会走serviceAuthorizationCheck
处理,其配置如下:
<!-- login-webflow.xml -->
<action-state id="serviceAuthorizationCheck">
<evaluate expression="serviceAuthorizationCheck"/>
<transition to="initializeLogin"/>
</action-state>
serviceAuthorizationCheck
对应的是ServiceAuthorizationCheck
,继承自AbstractAction
,实现了算法细节doExecute
,代码如下:
protected Event doExecute(final RequestContext context) throws Exception {
// 从上下文中获取Service信息
final Service service = WebUtils.getService(context);
//No service == plain /login request. Return success indicating transition to the login form
if (service == null) {
// 不存在Service直接返回
return success();
}
if (this.servicesManager.getAllServices().isEmpty()) {
// 未配置任何Service
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);
}
// 获取匹配的已注册Service
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
if (registeredService == null) {
// 无匹配的已注册Service,则授权校验失败
final String msg = String.format("Service Management: 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("Service Management: Unauthorized Service Access. "
+ "Service [%s] is not allowed access via the service registry.", service.getId());
logger.warn(msg);
// 设置重定向URL
WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context,
registeredService.getAccessStrategy().getUnauthorizedRedirectUrl());
throw new UnauthorizedServiceException(UnauthorizedServiceException.CODE_UNAUTHZ_SERVICE, msg);
}
return success();
}
login请求未携带Service
信息,所以走initializeLogin
处理,其配置如下:
<!-- login-webflow.xml -->
<action-state id="initializeLogin">
<evaluate expression="'success'"/>
<transition on="success" to="viewLoginForm"/>
</action-state>
根据表达式的值,直接走viewLoginForm
处理,其配置如下:
<!-- login-webflow.xml -->
<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>
<view-state id="viewLoginForm" view="casLoginView" model="credential">
<binder>
<binding property="username" required="true"/>
<binding property="password" required="true"/>
<!--
<binding property="rememberMe" />
-->
</binder>
<on-entry>
<set name="viewScope.commandName" value="'credential'"/>
<!--
<evaluate expression="samlMetadataUIParserAction" />
-->
</on-entry>
<transition on="submit" bind="true" validate="true" to="realSubmit"/>
</view-state>
根据urlBasedViewResolver
的配置:
<!-- cas-servlet.xml -->
<bean id="urlBasedViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"
p:viewClass="org.springframework.web.servlet.view.InternalResourceView"
p:prefix="${cas.themeResolver.pathprefix:/WEB-INF/view/jsp}/"
p:suffix=".jsp"
p:order="2000"/>
以及RegisteredServiceThemeBasedViewResolver.THEME_LOCATION_PATTERN
(即%s/%s/ui/
)参数值,通过代码
// viewName为casLoginView
final String defaultThemePrefix = String.format(THEME_LOCATION_PATTERN, getPrefix(), "default");
final String defaultViewUrl = defaultThemePrefix + viewName + getSuffix();
可知,casLoginView
对应的页面为/WEB-INF/view/jsp/default/ui/casLoginView.jsp
,该页面主要内容如下:
<form:input cssClass="required" cssErrorClass="error" id="username" size="25" tabindex="1" accesskey="${userNameAccessKey}" path="username" autocomplete="off" htmlEscape="true" />
<form:password cssClass="required" cssErrorClass="error" id="password" size="25" tabindex="2" path="password" accesskey="${passwordAccessKey}" htmlEscape="true" autocomplete="off" />
<input type="hidden" name="execution" value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
<input class="btn-submit" name="submit" accesskey="l" value="<spring:message code="screen.welcome.button.login" />" tabindex="6" type="submit" />
<input class="btn-reset" name="reset" accesskey="c" value="<spring:message code="screen.welcome.button.clear" />" tabindex="7" type="reset" />
casLoginView.jsp
页面提交的参数绑定到UsernamePasswordCredential
对象中,页面提交后,走realSubmit
处理,其配置如下:
<!-- login-webflow.xml -->
<action-state id="realSubmit">
<evaluate
expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)"/>
<transition on="warn" to="warn"/>
<!--
To enable AUP workflows, replace the 'success' transition with the following:
<transition on="success" to="acceptableUsagePolicyCheck" />
-->
<transition on="success" to="sendTicketGrantingTicket"/>
<transition on="successWithWarnings" to="showMessages"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="initializeLogin"/>
</action-state>
authenticationViaFormAction
对应的是AuthenticationViaFormAction
,其submit
方法的代码如下:
public final Event submit(final RequestContext context, final Credential credential,
final MessageContext messageContext) {
if (isRequestAskingForServiceTicket(context)) {
// 若请求ST,则创建ST
return grantServiceTicket(context, credential);
}
// 创建TGT
return createTicketGrantingTicket(context, credential, messageContext);
}
protected boolean isRequestAskingForServiceTicket(final RequestContext context) {
// 从上下文中获取TGT信息
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
// 从上下文获取Service信息
final Service service = WebUtils.getService(context);
// 如果请求的"renew"属性存在,且TGT信息和Service信息都存在,才认为该请求用来获取ST
// login请求则不满足该判定条件
return (StringUtils.isNotBlank(context.getRequestParameters().get(CasProtocolConstants.PARAMETER_RENEW))
&& ticketGrantingTicketId != null
&& service != null);
}
protected Event createTicketGrantingTicket(final RequestContext context, final Credential credential,
final MessageContext messageContext) {
try {
final Service service = WebUtils.getService(context);
final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder(
this.authenticationSystemSupport.getPrincipalElectionStrategy());
// 包装页面提交的登录参数
final AuthenticationTransaction transaction =
AuthenticationTransaction.wrap(credential);
// 鉴权登录参数
this.authenticationSystemSupport.getAuthenticationTransactionManager().handle(transaction, builder);
final AuthenticationContext authenticationContext = builder.build(service);
// 校验通过后,创建TGT
final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationContext);
// 将TGT放入上下文
WebUtils.putTicketGrantingTicketInScopes(context, tgt);
WebUtils.putWarnCookieIfRequestParameterPresent(this.warnCookieGenerator, context);
putPublicWorkstationToFlowIfRequestParameterPresent(context);
if (addWarningMessagesToMessageContextIfNeeded(tgt, messageContext)) {
return newEvent(SUCCESS_WITH_WARNINGS);
}
return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_SUCCESS);
} catch (final AuthenticationException e) {
logger.debug(e.getMessage(), e);
// 校验失败
return newEvent(AUTHENTICATION_FAILURE, e);
} catch (final Exception e) {
logger.debug(e.getMessage(), e);
// 流程处理异常
return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e);
}
}
核心逻辑主要如下:
- 包装登录参数;
- 鉴权登录参数;
- 创建TGT;
1 包装登录参数
其实就是将有效的Credential
包装成AuthenticationTransaction
,代码如下:
public static AuthenticationTransaction wrap(final Credential... credentials) {
return new AuthenticationTransaction(sanitizeCredentials(credentials));
}
private static Set<Credential> sanitizeCredentials(final Credential[] credentials) {
if (credentials != null && credentials.length > 0) {
final Set<Credential> set = new HashSet<>(Arrays.asList(credentials));
final Iterator<Credential> it = set.iterator();
while (it.hasNext()) {
if (it.next() == null) {
// 过滤无效值
it.remove();
}
}
return set;
}
return Collections.emptySet();
}
2 鉴权登录参数
借助PolicyBasedAuthenticationManager
实现鉴权,并将鉴权结果存入AuthenticationContextBuilder
,代码如下:
public AuthenticationTransactionManager handle(final AuthenticationTransaction authenticationTransaction,
final AuthenticationContextBuilder authenticationContext)
throws AuthenticationException {
if (!authenticationTransaction.getCredentials().isEmpty()) {
// 借助PolicyBasedAuthenticationManager完成鉴权
final Authentication authentication = this.authenticationManager.authenticate(authenticationTransaction);
LOGGER.debug("Successful authentication; Collecting authentication result [{}]", authentication);
// 收集鉴权结果
authenticationContext.collect(authentication);
}
LOGGER.debug("Transaction ignored since there are no credentials to authenticate");
return this;
}
PolicyBasedAuthenticationManager
实现了AuthenticationManager
接口,核心代码如下:
public Authentication authenticate(final AuthenticationTransaction transaction) throws AuthenticationException {
// 鉴权
final AuthenticationBuilder builder = authenticateInternal(transaction.getCredentials());
final Authentication authentication = builder.build();
final Principal principal = authentication.getPrincipal();
if (principal instanceof NullPrincipal) {
// 鉴权失败
throw new UnresolvedPrincipalException(authentication);
}
// 添加鉴权方法属性,即AuthenticationHandler
addAuthenticationMethodAttribute(builder, authentication);
logger.info("Authenticated {} with credentials {}.", principal, transaction.getCredentials());
logger.debug("Attribute map for {}: {}", principal.getId(), principal.getAttributes());
// 填充鉴权元数据属性
populateAuthenticationMetadataAttributes(builder, transaction.getCredentials());
// 构建Authentication
return builder.build();
}
protected AuthenticationBuilder authenticateInternal(final Collection<Credential> credentials)
throws AuthenticationException {
// 初始构造器
final AuthenticationBuilder builder = new DefaultAuthenticationBuilder(NullPrincipal.getInstance());
// 填充Credential数据
for (final Credential c : credentials) {
builder.addCredential(new BasicCredentialMetaData(c));
}
boolean found;
// 遍历Credential
for (final Credential credential : credentials) {
found = false;
// 遍历配置的AuthenticationHandler和PrincipalResolver MAP
for (final Map.Entry<AuthenticationHandler, PrincipalResolver> entry : this.handlerResolverMap.entrySet()) {
final AuthenticationHandler handler = entry.getKey();
// 当前AuthenticationHandler是否支持处理当前Credential
if (handler.supports(credential)) {
// 存在处理当前Credential的AuthenticationHandler
found = true;
try {
// 鉴权并解析Principal
authenticateAndResolvePrincipal(builder, credential, entry.getValue(), handler);
// 判定是否满足退出条件
if (this.authenticationPolicy.isSatisfiedBy(builder.build())) {
return builder;
}
} catch (final GeneralSecurityException e) {
logger.info("{} failed authenticating {}", handler.getName(), credential);
logger.debug("{} exception details: {}", handler.getName(), e.getMessage());
builder.addFailure(handler.getName(), e.getClass());
} catch (final PreventedException e) {
logger.error("{}: {} (Details: {})", handler.getName(), e.getMessage(), e.getCause().getMessage());
builder.addFailure(handler.getName(), e.getClass());
}
}
}
if (!found) {
logger.warn(
"Cannot find authentication handler that supports [{}] of type [{}], which suggests a configuration problem.",
credential, credential.getClass().getSimpleName());
}
}
// 校验生成的鉴权上下文
// 如果无鉴权成功的HandlerResult或者不满足AuthenticationPolicy的条件,则抛出对应异常
evaluateProducedAuthenticationContext(builder);
return builder;
}
核心逻辑主要如下:
- 遍历
Credential
集合,针对每个Credential
进行鉴权; - 针对当前
Credential
,遍历配置的handlerResolverMap
,找到匹配的AuthenticationHandler
; - 根据匹配到的
AuthenticationHandler
和PrincipalResolver
对当前Credential
进行鉴权;- 若鉴权成功,则根据
AuthenticationPolicy
的条件,判定是否结束鉴权;- 结束,直接返回鉴权结果;
- 不结束,则继续下一轮
AuthenticationHandler
和PrincipalResolver
,再继续下一轮Credential
;
- 若鉴权失败,
AuthenticationBuilder
记录失败信息;
- 若鉴权成功,则根据
- 遍历完
Credential
集合后,校验AuthenticationBuilder
;
2.1 遍历Credential集合
登录请求处理流程中,Credential
集合仅存在一个Credential
;
2.2 遍历配置的handlerResolverMap
handlerResolverMap
的配置信息如下:
<!-- deployerConfigContext.xml -->
<util:map id="authenticationHandlersResolvers">
<entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
<entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
</util:map>
<alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" />
<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />
acceptUsersAuthenticationHandler
对应的是AcceptUsersAuthenticationHandler
,其匹配逻辑继承自父类AbstractUsernamePasswordAuthenticationHandler
,代码如下:
public boolean supports(final Credential credential) {
// 是否是UsernamePasswordCredential类型,
return credential instanceof UsernamePasswordCredential;
}
proxyAuthenticationHandler
对应的是HttpBasedServiceCredentialsAuthenticationHandler
,其匹配逻辑如下:
public boolean supports(final Credential credential) {
return credential instanceof HttpBasedServiceCredential;
}
<-- login-webflow.xml -->
<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>
由credential
定义配置可知,AcceptUsersAuthenticationHandler
会处理本次login请求提交的Credential
,AcceptUsersAuthenticationHandler
不会处理本次login请求提交的Credential
;
2.3 鉴权Credential
AuthenticationHandler
和PrincipalResolver
对当前Credential
进行鉴权,代码如下:
private void authenticateAndResolvePrincipal(final AuthenticationBuilder builder, final Credential credential,
final PrincipalResolver resolver, final AuthenticationHandler handler)
throws GeneralSecurityException, PreventedException {
Principal principal;
// 由AuthenticationHandler完成鉴权
final HandlerResult result = handler.authenticate(credential);
// 鉴权成功,记录AuthenticationHandler信息
builder.addSuccess(handler.getName(), result);
logger.info("{} successfully authenticated {}", handler.getName(), credential);
if (resolver == null) {
// 解析器为空,则Principal取自HandlerResult
principal = result.getPrincipal();
logger.debug(
"No resolver configured for {}. Falling back to handler principal {}",
handler.getName(),
principal);
} else {
// 解析Principal
principal = resolvePrincipal(handler.getName(), resolver, credential);
if (principal == null) {
logger.warn("Principal resolution handled by {} produced a null principal. "
+ "This is likely due to misconfiguration or missing attributes; CAS will attempt to use the principal "
+ "produced by the authentication handler, if any.", resolver.getClass().getSimpleName());
// 解析无果,则Principal取自HandlerResult
principal = result.getPrincipal();
}
}
// Must avoid null principal since AuthenticationBuilder/ImmutableAuthentication
// require principal to be non-null
if (principal != null) {
// 设置Principal
builder.setPrincipal(principal);
}
logger.debug("Final principal resolved for this authentication event is {}", principal);
}
先由AuthenticationHandler
即AcceptUsersAuthenticationHandler
对当前Credential
进行鉴权处理,其父类AbstractPreAndPostProcessingAuthenticationHandler
实现了authenticate
的算法模板,并提供preAuthenticate
、postAuthenticate
算法细节的默认实现,可由子类重写,提供doAuthentication
算法细节,由子类实现,代码如下:
public final HandlerResult authenticate(final Credential credential)
throws GeneralSecurityException, PreventedException {
// 鉴权预处理
if (!preAuthenticate(credential)) {
throw new FailedLoginException();
}
// 鉴权后处理
return postAuthenticate(credential, doAuthentication(credential));
}
protected boolean preAuthenticate(final Credential credential) {
return true;
}
protected HandlerResult postAuthenticate(final Credential credential, final HandlerResult result) {
return result;
}
protected abstract HandlerResult doAuthentication(Credential credential)
throws GeneralSecurityException, PreventedException;
// 提供创建HandlerResult的接口供子类实现调用
protected final HandlerResult createHandlerResult(final Credential credential, final Principal principal,
final List<MessageDescriptor> warnings) {
return new DefaultHandlerResult(this, new BasicCredentialMetaData(credential), principal, warnings);
}
AcceptUsersAuthenticationHandlerde
的父类AbstractUsernamePasswordAuthenticationHandler
实现了抽象的doAuthentication
的算法细节的算法模板,并提供authenticateUsernamePasswordInternal
算法细节,由子类实现,其代码如下:
protected final HandlerResult doAuthentication(final Credential credential)
throws GeneralSecurityException, PreventedException {
final UsernamePasswordCredential userPass = (UsernamePasswordCredential) credential;
if (userPass.getUsername() == null) {
// 无用户名则抛出异常
throw new AccountNotFoundException("Username is null.");
}
// 支持转换用户名的策略模式
// 默认使用NoOpPrincipalNameTransformer,不转换
final String transformedUsername= this.principalNameTransformer.transform(userPass.getUsername());
if (transformedUsername == null) {
throw new AccountNotFoundException("Transformed username is null.");
}
// 更新为转换后的用户名
userPass.setUsername(transformedUsername);
// 鉴权转换后的数据
return authenticateUsernamePasswordInternal(userPass);
}
protected abstract HandlerResult authenticateUsernamePasswordInternal(UsernamePasswordCredential transformedCredential)
throws GeneralSecurityException, PreventedException;
AcceptUsersAuthenticationHandlerde
实现了authenticateUsernamePasswordInternal
算法细节,代码如下:
private static final String DEFAULT_SEPARATOR = "::";
private static final Pattern USERS_PASSWORDS_SPLITTER_PATTERN = Pattern.compile(DEFAULT_SEPARATOR);
private Map<String, String> users;
@Value("${accept.authn.users:}")
private String acceptedUsers;
@PostConstruct
public void init() {
if (StringUtils.isNotBlank(this.acceptedUsers) && this.users == null) {
// 若配置的用户信息(由cas.properties配置)存在,则当前缓存的用户信息不存在,则使用配置的用户信息
// 配置信息格式:username::password
final Set<String> usersPasswords = org.springframework.util.StringUtils.commaDelimitedListToSet(this.acceptedUsers);
final Map<String, String> parsedUsers = new HashMap<>();
for (final String usersPassword : usersPasswords) {
final String[] splitArray = USERS_PASSWORDS_SPLITTER_PATTERN.split(usersPassword);
parsedUsers.put(splitArray[0], splitArray[1]);
}
setUsers(parsedUsers);
}
}
protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential)
throws GeneralSecurityException, PreventedException {
if (users == null || users.isEmpty()) {
// 当前用户信息不存在,则不支持登录
throw new FailedLoginException("No user can be accepted because none is defined");
}
final String username = credential.getUsername();
final String cachedPassword = this.users.get(username);
if (cachedPassword == null) {
// 用户对应的密码不存在,则账户不存在
logger.debug("{} was not found in the map.", username);
throw new AccountNotFoundException(username + " not found in backing map.");
}
// 加密提交的密码信息
// 默认使用PlainTextPasswordEncoder,即无加密
final String encodedPassword = this.getPasswordEncoder().encode(credential.getPassword());
// 对比密码信息
if (!cachedPassword.equals(encodedPassword)) {
throw new FailedLoginException();
}
// 密码校验通过,则创建HandlerResult
// 默认使用DefaultPrincipalFactory创建SimplePrincipal,其属性为空MAP
return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null);
}
public final void setUsers(@NotNull final Map<String, String> users) {
this.users = Collections.unmodifiableMap(users);
}
用户信息由两种方式传入:
- 通过
setUsers
方式注入,优先级高;
<!-- deployerConfigContext.xml -->
<bean id="acceptUsersAuthenticationHandler"
class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
<property name="users">
<map>
<entry key="casuser" value="Mellon"/>
</map>
</property>
</bean>
- 通过
cas.properties
配置,优先级低;
accept.authn.users=casuser::Mellon
若AuthenticationHandler
鉴权失败,则抛异常结束;若AuthenticationHandler
鉴权成功,则使用PrincipalResolver
解析当前Credential
的Principal
信息,代码如下:
protected Principal resolvePrincipal(
final String handlerName, final PrincipalResolver resolver, final Credential credential) {
// 当前PrincipalResolver是否支持解析Credential
if (resolver.supports(credential)) {
try {
// 解析当前Credential的Principal信息
final Principal p = resolver.resolve(credential);
logger.debug("{} resolved {} from {}", resolver, p, credential);
return p;
} catch (final Exception e) {
logger.error("{} failed to resolve principal from {}", resolver, credential, e);
}
} else {
logger.warn(
"{} is configured to use {} but it does not support {}, which suggests a configuration problem.",
handlerName,
resolver,
credential);
}
return null;
}
由primaryPrincipalResolver
的配置:
<!-- deployerConfigContext.xml -->
<alias name="personDirectoryPrincipalResolver" alias="primaryPrincipalResolver" />
可知,personDirectoryPrincipalResolver
对应的是PersonDirectoryPrincipalResolver
,会处理本次login请求提交的Credential
,代码如下:
/**
* 本PrincipalResolver是否支持处理Credential
*/
public boolean supports(final Credential credential) {
// 只要Credential的ID存在就支持
return credential != null && credential.getId() != null;
}
/**
* 解析Credential
*/
public Principal resolve(final Credential credential) {
logger.debug("Attempting to resolve a principal...");
final String principalId = extractPrincipalId(credential);
// 校验principalId
if (principalId == null) {
logger.debug("Got null for extracted principal ID; returning null.");
return null;
}
logger.debug("Creating SimplePrincipal for [{}]", principalId);
// 获取principalId对应的属性集合
final Map<String, List<Object>> attributes = retrievePersonAttributes(principalId, credential);
// 校验属性集合
if (attributes == null || attributes.isEmpty()) {
logger.debug("Principal id [{}] did not specify any attributes", principalId);
// 是否直接返回null
if (!this.returnNullIfNoAttributes) {
logger.debug("Returning the principal with id [{}] without any attributes", principalId);
// 返回空属性Principal
return this.principalFactory.createPrincipal(principalId);
}
logger.debug("[{}] is configured to return null if no attributes are found for [{}]",
this.getClass().getName(), principalId);
return null;
}
logger.debug("Retrieved [{}] attribute(s) from the repository", attributes.size());
// 转换属性集合,可能会更新principalId参数,若配置了this.principalAttributeName属性
final Pair<String, Map<String, Object>> pair = convertPersonAttributesToPrincipal(principalId, attributes);
// 根据principalId和属性集合创建Principal
return this.principalFactory.createPrincipal(pair.getFirst(), pair.getSecond());
}
protected String extractPrincipalId(final Credential credential) {
return credential.getId();
}
protected Map<String, List<Object>> retrievePersonAttributes(final String principalId, final Credential credential) {
// 从IPersonAttributeDao中查询用户属性
final IPersonAttributes personAttributes = this.attributeRepository.getPerson(principalId);
final Map<String, List<Object>> attributes;
// 校验查询到的用户属性
if (personAttributes == null) {
attributes = null;
} else {
attributes = personAttributes.getAttributes();
}
return attributes;
}
protected Pair<String, Map<String, Object>> convertPersonAttributesToPrincipal(final String extractedPrincipalId,
final Map<String, List<Object>> attributes) {
final Map<String, Object> convertedAttributes = new HashMap<>();
// 默认的principalId
String principalId = extractedPrincipalId;
for (final Map.Entry<String, List<Object>> entry : attributes.entrySet()) {
final String key = entry.getKey();
final List<Object> values = entry.getValue();
if (StringUtils.isNotBlank(this.principalAttributeName)
&& key.equalsIgnoreCase(this.principalAttributeName)) {
// 如果配置了this.principalAttributeName属性,则采用该属性值对应的第一个属性作为principalId
if (values.isEmpty()) {
logger.debug("{} is empty, using {} for principal", this.principalAttributeName, extractedPrincipalId);
} else {
// 更新principalId
principalId = values.get(0).toString();
logger.debug(
"Found principal attribute value {}; removing {} from attribute map.",
extractedPrincipalId,
this.principalAttributeName);
}
} else {
// 保存属性值
convertedAttributes.put(key, values.size() == 1 ? values.get(0) : values);
}
}
return new Pair<>(principalId, convertedAttributes);
}
首先分析获取用户属性集合的方法:retrievePersonAttributes
,该方法借助this.attributeRepository
属性完成,该属性的赋值逻辑的核心代码如下:
// 默认使用StubPersonAttributeDao
protected IPersonAttributeDao attributeRepository = new StubPersonAttributeDao(new HashMap<String, List<Object>>());
// 可通过Setter注入自定义实现
public final void setAttributeRepository(@Qualifier("attributeRepository")
final IPersonAttributeDao attributeRepository) {
this.attributeRepository = attributeRepository;
}
attributeRepository
在deployerConfigContext.xml
配置文件中有定义,配置如下:
<!-- deployerConfigContext.xml -->
<bean id="attributeRepository" class="org.jasig.services.persondir.support.NamedStubPersonAttributeDao"
p:backingMap-ref="attrRepoBackingMap" />
<util:map id="attrRepoBackingMap">
<entry key="uid" value="uid" />
<entry key="eduPersonAffiliation" value="eduPersonAffiliation" />
<entry key="groupMembership" value="groupMembership" />
<entry>
<key><value>memberOf</value></key>
<list>
<value>faculty</value>
<value>staff</value>
<value>org</value>
</list>
</entry>
</util:map>
NamedStubPersonAttributeDao
的getPerson
实现继承自父类StubPersonAttributeDao
,代码如下:
public IPersonAttributes getPerson(String uid) {
if (uid == null) {
throw new IllegalArgumentException("Illegal to invoke getPerson(String) with a null argument.");
} else {
// 直接返回this.backingPerson,即配置文件中的"attrRepoBackingMap"
return this.backingPerson;
}
}
再分析convertPersonAttributesToPrincipal
的逻辑,该方法中会根据已配置的this.principalAttributeName
属性(若配置的话),从属性值对应的属性集合中取出第一个值更新principalId信息,该属性的赋值逻辑的核心代码如下:
// 可选属性值
protected String principalAttributeName;
// 可通过Setter注入自定义实现
public void setPrincipalAttributeName(@Value("${cas.principal.resolver.persondir.principal.attribute:}")
final String attribute) {
this.principalAttributeName = attribute;
}
该参数可配置在cas.properties
文件中,默认值为:
#cas.principal.resolver.persondir.principal.attribute=cn
2.3.1 鉴权成功
鉴权成功后,会经过this.authenticationPolicy
判断是否可以结束当前鉴权流程,该属性的赋值逻辑的核心代码如下:
// 默认是AnyAuthenticationPolicy,默认是只要有一个成功鉴权的Credential就满足
private AuthenticationPolicy authenticationPolicy = new AnyAuthenticationPolicy();
// 可通过Setter注入自定义实现
@Resource(name="authenticationPolicy")
public void setAuthenticationPolicy(final AuthenticationPolicy policy) {
this.authenticationPolicy = policy;
}
分析下AuthenticationPolicy
的isSatisfiedBy
的实现,代码如下:
// 默认false
private boolean tryAll;
// 可通过Setter注入自定义实现
public void setTryAll(@Value("${cas.authn.policy.any.tryall:false}") final boolean tryAll) {
this.tryAll = tryAll;
}
public boolean isSatisfiedBy(final Authentication authn) {
// 是否需要遍历全部,默认false,用户可通过cas.properties配置
if (this.tryAll) {
return authn.getCredentials().size() == authn.getSuccesses().size() + authn.getFailures().size();
}
// 只要有一个鉴权成功的Credential
return !authn.getSuccesses().isEmpty();
}
2.3.2 鉴权失败
鉴权失败后,AuthenticationBuilder
记录失败信息,然后继续下一轮AuthenticationHandler
和PrincipalResolver
,再继续下一轮Credential
;
2.4 校验AuthenticationBuilder
遍历完Credential
集合后,正常退出前,会校验AuthenticationBuilder
的有效性,如果没有鉴权成功的Credential
,或者不满足this.authenticationPolicy
的结束条件,则抛出异常,代码如下:
private void evaluateProducedAuthenticationContext(final AuthenticationBuilder builder) throws AuthenticationException {
// We apply an implicit security policy of at least one successful authentication
if (builder.getSuccesses().isEmpty()) {
throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
}
// Apply the configured security policy
if (!this.authenticationPolicy.isSatisfiedBy(builder.build())) {
throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
}
}
authenticateInternal
鉴权结束后,会再次校验鉴权结果的Principal
的有效性(鉴权过程中可能会返回NullPrincipal
),然后添加AuthenticationHandler
的信息,再经过配置的this.authenticationMetaDataPopulators
,填充元数据属性,最后返回鉴权结果;
至此,鉴权流程结束;
3 创建TGT
鉴权成功后,就会使用this.centralAuthenticationService
属性创建TGT信息,该属性的赋值逻辑的核心代码如下:
@Autowired
@Qualifier("centralAuthenticationService")
private CentralAuthenticationService centralAuthenticationService;
centralAuthenticationService
对应的是CentralAuthenticationServiceImpl
,其创建TGT的代码如下:
public TicketGrantingTicket createTicketGrantingTicket(final AuthenticationContext context)
throws AuthenticationException, AbstractTicketException {
// 获取鉴权结果
final Authentication authentication = context.getAuthentication();
// 获取TGT创建工厂
final TicketGrantingTicketFactory factory = this.ticketFactory.get(TicketGrantingTicket.class);
// 根据鉴权结果创建TGT
final TicketGrantingTicket ticketGrantingTicket = factory.create(authentication);
// 缓存创建的TGT
this.ticketRegistry.addTicket(ticketGrantingTicket);
// 发布TGT创建事件
doPublishEvent(new CasTicketGrantingTicketCreatedEvent(this, ticketGrantingTicket));
return ticketGrantingTicket;
}
this.ticketFactory
属性保存了所欲配置的TicketFactory
信息,该属性的赋值逻辑的核心代码如下:
@Resource(name="defaultTicketFactory")
protected TicketFactory ticketFactory;
defaultTicketFactory
对应的是DefaultTicketFactory
,核心代码是:
// 保存各种TicketFactory的实例
private Map<String, Object> factoryMap;
@Autowired
@Qualifier("defaultProxyTicketFactory")
private ProxyTicketFactory proxyTicketFactory;
@Autowired
@Qualifier("defaultServiceTicketFactory")
private ServiceTicketFactory serviceTicketFactory;
@Autowired
@Qualifier("defaultTicketGrantingTicketFactory")
private TicketGrantingTicketFactory ticketGrantingTicketFactory;
@Autowired
@Qualifier("defaultProxyGrantingTicketFactory")
private ProxyGrantingTicketFactory proxyGrantingTicketFactory;
@PostConstruct
public void initialize() {
this.factoryMap = new HashMap<>();
validateFactoryInstances();
this.factoryMap.put(ProxyGrantingTicket.class.getCanonicalName(), this.proxyGrantingTicketFactory);
this.factoryMap.put(TicketGrantingTicket.class.getCanonicalName(), this.ticketGrantingTicketFactory);
this.factoryMap.put(ServiceTicket.class.getCanonicalName(), this.serviceTicketFactory);
this.factoryMap.put(ProxyTicket.class.getCanonicalName(), this.proxyTicketFactory);
}
public <T extends TicketFactory> T get(final Class<? extends Ticket> clazz) {
validateFactoryInstances();
// 根据class从缓存的factoryMap中找到对应的实例
return (T) this.factoryMap.get(clazz.getCanonicalName());
}
通过this.ticketFactory.get(TicketGrantingTicket.class)
可以找到defaultTicketGrantingTicketFactory
,即DefaultTicketGrantingTicketFactory
,然后调用其create
方法创建TGT,代码如下:
public <T extends TicketGrantingTicket> T create(final Authentication authentication) {
final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(TicketGrantingTicket.PREFIX),
authentication, ticketGrantingTicketExpirationPolicy);
return (T) ticketGrantingTicket;
}
最终创建的TGT为TicketGrantingTicketImpl
,其id值由this.ticketGrantingTicketUniqueTicketIdGenerator
属性生成,该属性的赋值逻辑的核心代码如下:
@NotNull
@Resource(name="ticketGrantingTicketUniqueIdGenerator")
protected UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator;
ticketGrantingTicketUniqueIdGenerator
对应的是TicketGrantingTicketIdGenerator
,其getNewTicketId
方法继承自父类DefaultUniqueTicketIdGenerator
,代码如下:
public final String getNewTicketId(final String prefix) {
final String number = this.numericGenerator.getNextNumberAsString();
final StringBuilder buffer = new StringBuilder(prefix.length() + 2
+ (StringUtils.isNotBlank(this.suffix) ? this.suffix.length() : 0) + this.randomStringGenerator.getMaxLength()
+ number.length());
buffer.append(prefix);
buffer.append('-');
buffer.append(number);
buffer.append('-');
buffer.append(this.randomStringGenerator.getNewString());
if (this.suffix != null) {
buffer.append(this.suffix);
}
return buffer.toString();
}
至此,TGT的创建流程结束;
成功创建出TGT后,就会走success
处理,即gatewayRequestCheck
,其配置如下:
<!-- login-webflow.xml -->
<action-state id="sendTicketGrantingTicket">
<evaluate expression="sendTicketGrantingTicketAction"/>
<transition to="serviceCheck"/>
</action-state>
sendTicketGrantingTicketAction
对应的是SendTicketGrantingTicketAction
,继承自AbstractAction
,实现了算法细节doExecute
,代码如下:
protected Event doExecute(final RequestContext context) {
// 从上下文中获取TGT
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
// 从cookie中获取TGT
final String ticketGrantingTicketValueFromCookie = (String) context.getFlowScope().get("ticketGrantingTicketId");
if (ticketGrantingTicketId == null) {
// 上下文中不存在TGT直接返回
return success();
}
if (isAuthenticatingAtPublicWorkstation(context)) {
// 通过公共工作台去认证的,将不产生cookie
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 {
LOGGER.debug("Setting TGC for current session.");
// 设置TGT cookie
this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils
.getHttpServletResponse(context), ticketGrantingTicketId);
}
if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
// 原cookie中的TGT和上下文中TGT不一样,则销毁原cookie中的TGT
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
}
return success();
}
销毁TGT由CentralAuthenticationServiceImpl
实现,代码如下:
public List<LogoutRequest> destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) {
try {
logger.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId);
// 根据TGT ID查询出TGT
final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
logger.debug("Ticket found. Processing logout requests and then deleting the ticket…");
// 对使用该TGT的单点登出服务做登出操作
final List<LogoutRequest> logoutRequests = logoutManager.performLogout(ticket);
// 从缓存中删除TGT
this.ticketRegistry.deleteTicket(ticketGrantingTicketId);
// 发布TGT销毁事件
doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket));
return logoutRequests;
} catch (final InvalidTicketException e) {
logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
}
return Collections.emptyList();
}
logoutManager.performLogout
由LogoutManagerImpl
实现,代码如下:
public List<LogoutRequest> performLogout(final TicketGrantingTicket ticket) {
// 获取当前TGT绑定的Service集合
final Map<String, Service> services = ticket.getServices();
final List<LogoutRequest> logoutRequests = new ArrayList<>();
// if SLO is not disabled
if (!this.singleLogoutCallbacksDisabled) {
// 开启SLO功能
// through all services
for (final Map.Entry<String, Service> entry : services.entrySet()) {
// it's a SingleLogoutService, else ignore
final Service service = entry.getValue();
if (service instanceof SingleLogoutService) {
// 是单点登出服务则需要执行登出
final LogoutRequest logoutRequest = handleLogoutForSloService((SingleLogoutService) service, entry.getKey());
if (logoutRequest != null) {
LOGGER.debug("Captured logout request [{}]", logoutRequest);
logoutRequests.add(logoutRequest);
}
}
}
}
return logoutRequests;
}
private LogoutRequest handleLogoutForSloService(final SingleLogoutService singleLogoutService, final String ticketId) {
// 服务是否已登出
if (!singleLogoutService.isLoggedOutAlready()) {
// 找到已注册的服务
final RegisteredService registeredService = servicesManager.findServiceBy(singleLogoutService);
// 判断服务是否支持单点登出
if (serviceSupportsSingleLogout(registeredService)) {
// 获取服务的登出URL
final URL logoutUrl = determineLogoutUrl(registeredService, singleLogoutService);
// 封装登出请求
final DefaultLogoutRequest logoutRequest = new DefaultLogoutRequest(ticketId, singleLogoutService, logoutUrl);
// 获取服务的登出类型
final LogoutType type = registeredService.getLogoutType() == null
? LogoutType.BACK_CHANNEL : registeredService.getLogoutType();
// 根据服务的登出类型执行相应操作
switch (type) {
case BACK_CHANNEL:
// 尝试发送登出请求
if (performBackChannelLogout(logoutRequest)) {
logoutRequest.setStatus(LogoutRequestStatus.SUCCESS);
} else {
logoutRequest.setStatus(LogoutRequestStatus.FAILURE);
LOGGER.warn("Logout message not sent to [{}]; Continuing processing...", singleLogoutService.getId());
}
break;
default:
// 其他类型,则不支持反向通道登出操作
logoutRequest.setStatus(LogoutRequestStatus.NOT_ATTEMPTED);
break;
}
return logoutRequest;
}
}
return null;
}
private boolean serviceSupportsSingleLogout(final RegisteredService registeredService) {
return registeredService != null
&& registeredService.getAccessStrategy().isServiceAccessAllowed()
&& registeredService.getLogoutType() != LogoutType.NONE;
}
private URL determineLogoutUrl(final RegisteredService registeredService, final SingleLogoutService singleLogoutService) {
try {
URL logoutUrl = new URL(singleLogoutService.getOriginalUrl());
final URL serviceLogoutUrl = registeredService.getLogoutUrl();
if (serviceLogoutUrl != null) {
LOGGER.debug("Logout request will be sent to [{}] for service [{}]",
serviceLogoutUrl, singleLogoutService);
// 优先使用已注册服务的登出URL
logoutUrl = serviceLogoutUrl;
}
return logoutUrl;
} catch (final Exception e) {
throw new IllegalArgumentException(e);
}
}
private boolean performBackChannelLogout(final LogoutRequest request) {
try {
final String logoutRequest = this.logoutMessageBuilder.create(request);
final SingleLogoutService logoutService = request.getService();
// 设置服务的登出标识
logoutService.setLoggedOutAlready(true);
LOGGER.debug("Sending logout request for: [{}]", logoutService.getId());
final LogoutHttpMessage msg = new LogoutHttpMessage(request.getLogoutUrl(), logoutRequest);
LOGGER.debug("Prepared logout message to send is [{}]", msg);
// 发送登出请求
return this.httpClient.sendMessageToEndPoint(msg);
} catch (final Exception e) {
LOGGER.error(e.getMessage(), e);
}
return false;
}
login请求中cookie不会携带TGT信息,直接返回success()
,走serviceCheck
处理,其配置如下:
<!-- login-webflow.xml -->
<decision-state id="serviceCheck">
<if test="flowScope.service != null" then="generateServiceTicket" else="viewGenericLoginSuccess"/>
</decision-state>
login请求中cookie不会携带Service信息,走viewGenericLoginSuccess
处理,其配置如下:
<!-- login-webflow.xml -->
<end-state id="viewGenericLoginSuccess" view="casGenericSuccessView">
<on-entry>
<evaluate expression="genericSuccessViewAction.getAuthenticationPrincipal(flowScope.ticketGrantingTicketId)"
result="requestScope.principal"
result-type="org.jasig.cas.authentication.principal.Principal"/>
</on-entry>
</end-state>
casGenericSuccessView
对应的页面为/WEB-INF/view/jsp/default/ui/casGenericSuccessView.jsp
;
genericSuccessViewAction
对应的是GenericSuccessViewAction
,其getAuthenticationPrincipal
方法的代码如下:
public Principal getAuthenticationPrincipal(final String ticketGrantingTicketId) {
try {
// 根据TGT ID获取TGT信息
final TicketGrantingTicket ticketGrantingTicket =
this.centralAuthenticationService.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
// 返回TGT中鉴权结果的Principal信息
return ticketGrantingTicket.getAuthentication().getPrincipal();
} catch (final InvalidTicketException e){
logger.warn(e.getMessage());
}
logger.debug("In the absence of valid TGT, the authentication principal cannot be determined. Returning {}",
NullPrincipal.class.getSimpleName());
// 异常情况返回空Principal
return NullPrincipal.getInstance();
}
GenericSuccessViewAction
的getAuthenticationPrincipal
方法的返回结果会体现在页面/WEB-INF/view/jsp/default/ui/casGenericSuccessView.jsp
上,该页面主要内容如下:
<div id="msg" class="success">
<h2><spring:message code="screen.success.header" /></h2>
<p><spring:message code="screen.success.success" arguments="${principal.id}"/></p>
<p><spring:message code="screen.success.security" /></p>
</div>
至此,未携带Service
的login请求的处理流程结束。