概述
相关阅读
- CAS基础组件 简介
- CAS流程简析 服务端处理未携带Service登录请求
- CAS流程简析 服务端校验Ticket
- CAS流程简析 客户端处理携带Ticket访问请求
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
的配置如下:
<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);
// 默认为DefaultRegisteredServiceAccessStrategy,支持访问
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");
}
针对login请求中携带的Service
信息的处理如下:
- 从请求上下文中获取
Service
对象; - 若
Service
对象存在,则- 根据
Service
对象查找系统中注册的Service
信息; - 若已注册的
Service
信息存在且允许访问,则- 保存已注册
Service
信息到请求上下文中; - 若已注册
Service
信息存在未授权URL,则保存未授权URL到请求上下文中;
- 保存已注册
- 根据
- 若
Service
对象不存在,则判断当前是否支持处理不携带Service
的请求,- 若不支持,则抛异常;
- 保存
Service
对象到请求上下文; - 返回
success
;
接下来重点分析:
- 从请求上下文中获取
Service
对象; - 根据
Service
对象查找系统中注册的Service
信息;
1 从请求上下文中获取Service对象
代码如下:
final Service service = WebUtils.getService(this.argumentExtractors, context);
@Resource(name="argumentExtractors")
public void setArgumentExtractors(final List<ArgumentExtractor> argumentExtractors) {
this.argumentExtractors = argumentExtractors;
}
// WebUtils.java
public static WebApplicationService getService(
final List<ArgumentExtractor> argumentExtractors,
final RequestContext context) {
final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
return getService(argumentExtractors, request);
}
public static WebApplicationService getService(
final List<ArgumentExtractor> argumentExtractors,
final HttpServletRequest request) {
// 遍历ArgumentExtractor集合
for (final ArgumentExtractor argumentExtractor : argumentExtractors) {
final WebApplicationService service = argumentExtractor
.extractService(request);
if (service != null) {
return service;
}
}
return null;
}
根据defaultArgumentExtractor
的配置:
<!-- services-context.xml -->
<util:list id="argumentExtractors">
<ref bean="defaultArgumentExtractor"/>
</util:list>
可知,defaultArgumentExtractor
对应的是DefaultArgumentExtractor
,其extractService
继承自父类AbstractArgumentExtractor
,AbstractArgumentExtractor
实现了extractService
的算法模板,提供算法细节extractServiceInternal
由子类实现实现,还提供了serviceFactoryList
供子类实现使用,其代码如下:
public final WebApplicationService extractService(final HttpServletRequest request) {
// 从request中抽取Service信息
final WebApplicationService service = extractServiceInternal(request);
// log
if (service == null) {
logger.debug("Extractor did not generate service.");
} else {
logger.debug("Extractor generated service for: {}", service.getId());
}
return service;
}
protected abstract WebApplicationService extractServiceInternal(HttpServletRequest request);
@Resource(name="serviceFactoryList")
protected List<ServiceFactory<? extends WebApplicationService>> serviceFactoryList;
protected final List<ServiceFactory<? extends WebApplicationService>> getServiceFactories() {
return serviceFactoryList;
}
serviceFactoryList
的配置如下:
<!-- services-context.xml -->
<util:list id="serviceFactoryList" value-type="org.jasig.cas.authentication.principal.ServiceFactory">
<ref bean="webApplicationServiceFactory" />
</util:list>
DefaultArgumentExtractor
实现了算法细节extractServiceInternal
,代码如下:
public WebApplicationService extractServiceInternal(final HttpServletRequest request) {
for (final ServiceFactory<? extends WebApplicationService> factory : getServiceFactories()) {
final WebApplicationService service = factory.createService(request);
if (service != null) {
// 创建成功则直接返回
logger.debug("Created {} based on {}", service, factory);
return service;
}
}
logger.debug("No service could be extracted based on the given request");
return null;
}
webApplicationServiceFactory
对应的是WebApplicationServiceFactory
,其createService
方法代码如下:
public WebApplicationService createService(final HttpServletRequest request) {
final String targetService = request.getParameter(CasProtocolConstants.PARAMETER_TARGET_SERVICE);
final String service = request.getParameter(CasProtocolConstants.PARAMETER_SERVICE);
final String serviceAttribute = (String) request.getAttribute(CasProtocolConstants.PARAMETER_SERVICE);
final String method = request.getParameter(CasProtocolConstants.PARAMETER_METHOD);
final String format = request.getParameter(CasProtocolConstants.PARAMETER_FORMAT);
final String serviceToUse;
if (StringUtils.isNotBlank(targetService)) {
// 优先使用targetService
serviceToUse = targetService;
} else if (StringUtils.isNotBlank(service)) {
// 其次使用请求参数中的service
serviceToUse = service;
} else {
// 最后使用请求属性中的service
serviceToUse = serviceAttribute;
}
// 校验service信息
if (StringUtils.isBlank(serviceToUse)) {
return null;
}
// 去除jsession信息
final String id = AbstractServiceFactory.cleanupUrl(serviceToUse);
// 获取请求参数中的ticket信息,并将其作为Service的artifactId
final String artifactId = request.getParameter(CasProtocolConstants.PARAMETER_TICKET);
final Response.ResponseType type = HttpMethod.POST.name().equalsIgnoreCase(method) ? Response.ResponseType.POST
: Response.ResponseType.REDIRECT;
// 创建SimpleWebApplicationServiceImpl
final SimpleWebApplicationServiceImpl webApplicationService =
new SimpleWebApplicationServiceImpl(id, serviceToUse,
artifactId, new WebApplicationServiceResponseBuilder(type));
try {
if (StringUtils.isNotBlank(format)) {
// 若请求参数中存在format信息,则设置该属性
final ValidationResponseType formatType = ValidationResponseType.valueOf(format.toUpperCase());
webApplicationService.setFormat(formatType);
}
} catch (final Exception e) {
logger.error("Format specified in the request [{}] is not recognized", format);
return null;
}
return webApplicationService;
}
WebApplicationServiceFactory
创建的Service
为SimpleWebApplicationServiceImpl
,支持单点登出;
public final class SimpleWebApplicationServiceImpl extends AbstractWebApplicationService
public abstract class AbstractWebApplicationService implements SingleLogoutService
public interface SingleLogoutService extends WebApplicationService
public interface WebApplicationService extends Service
2 根据Service对象查找系统中注册的Service信息
代码如下:
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
private ServicesManager servicesManager;
@Autowired
public void setServicesManager(@Qualifier("servicesManager") final ServicesManager servicesManager) {
this.servicesManager = servicesManager;
}
servicesManager
对应的是DefaultServicesManagerImpl
,findServiceBy
方法的代码如下:
public RegisteredService findServiceBy(final Service service) {
final Collection<RegisteredService> c = convertToTreeSet();
// 遍历注册的Service
for (final RegisteredService r : c) {
if (r.matches(service)) {
// 若匹配则返回该注册的Service
return r;
}
}
return null;
}
public TreeSet<RegisteredService> convertToTreeSet() {
return new TreeSet<>(this.services.values());
}
private ConcurrentHashMap<Long, RegisteredService> services = new ConcurrentHashMap<>();
public void load() {
final ConcurrentHashMap<Long, RegisteredService> localServices =
new ConcurrentHashMap<>();
// 借助this.serviceRegistryDao加载注册Service信息
for (final RegisteredService r : this.serviceRegistryDao.load()) {
LOGGER.debug("Adding registered service {}", r.getServiceId());
localServices.put(r.getId(), r);
}
this.services = localServices;
LOGGER.info("Loaded {} services from {}.", this.services.size(),
this.serviceRegistryDao);
}
public DefaultServicesManagerImpl(@Qualifier("serviceRegistryDao") final ServiceRegistryDao serviceRegistryDao) {
this.serviceRegistryDao = serviceRegistryDao;
load();
}
ServiceRegistryDao
接口有多种实现,可根据实际需求,在配置文件中自行配置该接口的实现;以InMemoryServiceRegistryDaoImpl
为例,分析load方法实现,代码如下:
public List<RegisteredService> load() {
return this.registeredServices;
}
@PostConstruct
public void afterPropertiesSet() {
final String[] aliases =
this.applicationContext.getAutowireCapableBeanFactory().getAliases("inMemoryServiceRegistryDao");
// 如果配置了"inMemoryServiceRegistryDao"
if (aliases.length > 0) {
LOGGER.debug("{} is used as the active service registry dao", this.getClass().getSimpleName());
try {
// 从IOC容器中找到"inMemoryRegisteredServices"的配置
final List<RegisteredService> list = (List<RegisteredService>)
this.applicationContext.getBean("inMemoryRegisteredServices", List.class);
if (list != null) {
LOGGER.debug("Loaded {} services from the application context for {}",
list.size(),
this.getClass().getSimpleName());
this.registeredServices = list;
}
} catch (final Exception e) {
LOGGER.debug("No registered services are defined for {}", this.getClass().getSimpleName());
}
}
}
inMemoryRegisteredServices
配置信息举例如下:
<util:list id="inMemoryRegisteredServices">
<bean class="org.jasig.cas.services.RegexRegisteredService"
p:id="0" p:name="HTTP and IMAP" p:description="Allows HTTP(S) and IMAP(S) protocols"
p:serviceId="^(https?|imaps?)://.*" p:evaluationOrder="10000001" >
<property name="attributeReleasePolicy">
<bean class="org.jasig.cas.services.ReturnAllAttributeReleasePolicy" />
</property>
</bean>
</util:list>
常用的RegisteredService
接口的实现类为RegexRegisteredService
,支持正则表达式,其match
方法实现代码如下:
public boolean matches(final Service service) {
if (this.servicePattern == null) {
this.servicePattern = RegexUtils.createPattern(this.serviceId);
}
// 根据serviceId的正则规则进行匹配
return service != null && this.servicePattern != null
&& this.servicePattern.matcher(service.getId()).matches();
}
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>
根据表达式可知,不携带gateway信息的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
信息能匹配已注册的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;
}
由credential
定义配置:
<!-- login-webflow.xml -->
<var name="credential" class="org.jasig.cas.authentication.UsernamePasswordCredential"/>
可知,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);
}
AcceptUsersAuthenticationHandler
的父类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
配置文件中有定义,配置如下:
<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
信息,走generateServiceTicket
处理,其配置如下:
<!-- login-webflow.xml -->
<action-state id="generateServiceTicket">
<evaluate expression="generateServiceTicketAction"/>
<transition on="success" to="warn"/>
<transition on="authenticationFailure" to="handleAuthenticationFailure"/>
<transition on="error" to="initializeLogin"/>
<transition on="gateway" to="gatewayServicesManagementCheck"/>
</action-state>
generateServiceTicketAction
对应的是GenerateServiceTicketAction
,继承自AbstractAction
,实现了算法细节doExecute
,代码如下:
protected Event doExecute(final RequestContext context) {
// 从上下文中获取Service信息
final Service service = WebUtils.getService(context);
// 从上下文中获取TGT信息
final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
try {
/**
* In the initial primary authentication flow, credentials are cached and available.
* Since they are authenticated as part of submission first, there is no need to doubly
* authenticate and verify credentials.
*
* In subsequent authentication flows where a TGT is available and only an ST needs to be
* created, there are no cached copies of the credential, since we do have a TGT available.
* So we will simply grab the available authentication and produce the final result based on that.
*/
// 根据TGT获取对应的鉴权结果
final Authentication authentication = ticketRegistrySupport.getAuthenticationFrom(ticketGrantingTicket);
if (authentication == null) {
// TGT无对应的鉴权结果则无效
throw new InvalidTicketException(new AuthenticationException(), ticketGrantingTicket);
}
final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder(
this.authenticationSystemSupport.getPrincipalElectionStrategy());
final AuthenticationContext authenticationContext = builder.collect(authentication).build(service);
// 根据TGT,Service等创建ST
final ServiceTicket serviceTicketId = this.centralAuthenticationService
.grantServiceTicket(ticketGrantingTicket, service, authenticationContext);
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
return success();
} catch (final AuthenticationException e) {
logger.error("Could not verify credentials to grant service ticket", e);
} catch (final AbstractTicketException e) {
if (e instanceof InvalidTicketException) {
// 销毁TGT
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
}
if (isGatewayPresent(context)) {
return result("gateway");
}
return newEvent(AbstractCasWebflowConfigurer.TRANSITION_ID_ERROR, e);
}
return error();
}
this.centralAuthenticationService
就是CentralAuthenticationServiceImpl
,在创建TGT时介绍过,这里介绍其grantServiceTicket
方法,代码如下:
public ServiceTicket grantServiceTicket(
final String ticketGrantingTicketId,
final Service service, final AuthenticationContext context)
throws AuthenticationException, AbstractTicketException {
// 根据TGT ID获取缓存的TGT信息
final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
// 根据Service信息匹配获取已注册的Service信息
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
// 校验已注册Service信息
verifyRegisteredServiceProperties(registeredService, service);
// 校验当前鉴权结果和TGT中缓存的鉴权结果是否一致
final Authentication currentAuthentication = evaluatePossibilityOfMixedPrincipals(context, ticketGrantingTicket);
// 校验TGT的使用记录和已注册Service的访问策略
if (ticketGrantingTicket.getCountOfUses() > 0 && !registeredService.getAccessStrategy().isServiceAccessAllowedForSso()) {
// 如果TGT已经绑定过ST且当前已注册的Service不支持SSO,则抛异常
logger.warn("Service [{}] is not allowed to use SSO.", service.getId());
throw new UnauthorizedSsoServiceException();
}
// 校验代理Service如有必要
// 本例不涉及
evaluateProxiedServiceIfNeeded(service, ticketGrantingTicket, registeredService);
// Perform security policy check by getting the authentication that satisfies the configured policy
// This throws if no suitable policy is found
// 获取满足安全策略的鉴权结果
// 默认使用AcceptAnyAuthenticationPolicyFactory,默认满足
getAuthenticationSatisfiedByPolicy(ticketGrantingTicket.getRoot(), new ServiceContext(service, registeredService));
// 获取TGT缓存的鉴权结果,以及TGT关联的ST的缓存的鉴权结果
final List<Authentication> authentications = ticketGrantingTicket.getChainedAuthentications();
// 获取最后一个鉴权结果的Principal
final Principal principal = authentications.get(authentications.size() - 1).getPrincipal();
// 默认为空
final RegisteredServiceAttributeReleasePolicy releasePolicy = registeredService.getAttributeReleasePolicy();
final Map<String, Object> principalAttrs;
if (releasePolicy != null) {
principalAttrs = releasePolicy.getAttributes(principal);
} else {
principalAttrs = new HashMap<>();
}
// 校验principal是否满足RegisteredServiceAttributeReleasePolicy的要求,默认principalAttrs为空,故满足
if (!registeredService.getAccessStrategy().doPrincipalAttributesAllowServiceAccess(principal.getId(), principalAttrs)) {
logger.warn("Cannot grant service ticket because Service [{}] is not authorized for use by [{}].",
service.getId(), principal);
throw new UnauthorizedServiceForPrincipalException();
}
// 获取ServiceTicketFactory实例,即DefaultServiceTicketFactory
final ServiceTicketFactory factory = this.ticketFactory.get(ServiceTicket.class);
// 根据TGT,Service创建ST
final ServiceTicket serviceTicket = factory.create(ticketGrantingTicket, service, currentAuthentication != null);
// 缓存ST
this.ticketRegistry.addTicket(serviceTicket);
logger.info("Granted ticket [{}] for service [{}] and principal [{}]",
serviceTicket.getId(), service.getId(), principal.getId());
// 发布ST创建事件
doPublishEvent(new CasServiceTicketGrantedEvent(this, ticketGrantingTicket, serviceTicket));
// 返回ST
return serviceTicket;
}
protected final void verifyRegisteredServiceProperties(final RegisteredService registeredService,
final Service service) throws UnauthorizedServiceException {
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);
}
}
private static Authentication evaluatePossibilityOfMixedPrincipals(final AuthenticationContext context,
final TicketGrantingTicket ticketGrantingTicket)
throws MixedPrincipalException {
Authentication currentAuthentication = null;
if (context != null) {
currentAuthentication = context.getAuthentication();
if (currentAuthentication != null) {
final Authentication original = ticketGrantingTicket.getAuthentication();
// 校验当前鉴权结果和TGT中缓存的鉴权结果是否一致
if (!currentAuthentication.getPrincipal().equals(original.getPrincipal())) {
throw new MixedPrincipalException(
currentAuthentication, currentAuthentication.getPrincipal(), original.getPrincipal());
}
ticketGrantingTicket.getSupplementalAuthentications().clear();
ticketGrantingTicket.getSupplementalAuthentications().add(currentAuthentication);
}
}
return currentAuthentication;
}
protected final void evaluateProxiedServiceIfNeeded(final Service service, final TicketGrantingTicket ticketGrantingTicket,
final RegisteredService registeredService) {
final Service proxiedBy = ticketGrantingTicket.getProxiedBy();
if (proxiedBy != null) {
logger.debug("TGT is proxied by [{}]. Locating proxy service in registry...", proxiedBy.getId());
final RegisteredService proxyingService = servicesManager.findServiceBy(proxiedBy);
if (proxyingService != null) {
logger.debug("Located proxying service [{}] in the service registry", proxyingService);
if (!proxyingService.getProxyPolicy().isAllowedToProxy()) {
logger.warn("Found proxying service {}, but it is not authorized to fulfill the proxy attempt made by {}",
proxyingService.getId(), service.getId());
throw new UnauthorizedProxyingException("Proxying is not allowed for registered service "
+ registeredService.getId());
}
} else {
logger.warn("No proxying service found. Proxy attempt by service [{}] (registered service [{}]) is not allowed.",
service.getId(), registeredService.getId());
throw new UnauthorizedProxyingException("Proxying is not allowed for registered service "
+ registeredService.getId());
}
} else {
logger.trace("TGT is not proxied by another service");
}
}
protected final Authentication getAuthenticationSatisfiedByPolicy(
final TicketGrantingTicket ticket, final ServiceContext context) throws AbstractTicketException {
final ContextualAuthenticationPolicy<ServiceContext> policy =
serviceContextAuthenticationPolicyFactory.createPolicy(context);
if (policy.isSatisfiedBy(ticket.getAuthentication())) {
return ticket.getAuthentication();
}
for (final Authentication auth : ticket.getSupplementalAuthentications()) {
if (policy.isSatisfiedBy(auth)) {
return auth;
}
}
throw new UnsatisfiedAuthenticationPolicyException(policy);
}
// AcceptAnyAuthenticationPolicyFactory.java
public ContextualAuthenticationPolicy<ServiceContext> createPolicy(final ServiceContext context) {
return new ContextualAuthenticationPolicy<ServiceContext>() {
@Override
public ServiceContext getContext() {
return context;
}
@Override
public boolean isSatisfiedBy(final Authentication authentication) {
return true;
}
};
}
DefaultServiceTicketFactory
的create
方法,代码如下:
public <T extends Ticket> T create(final TicketGrantingTicket ticketGrantingTicket,
final Service service,
final boolean credentialsProvided) {
final String uniqueTicketIdGenKey = service.getClass().getName();
UniqueTicketIdGenerator serviceTicketUniqueTicketIdGenerator = null;
// 尝试获取Service特定的UniqueTicketIdGenerator
if (this.uniqueTicketIdGeneratorsForService != null && !uniqueTicketIdGeneratorsForService.isEmpty()) {
logger.debug("Looking up service ticket id generator for [{}]", uniqueTicketIdGenKey);
serviceTicketUniqueTicketIdGenerator = this.uniqueTicketIdGeneratorsForService.get(uniqueTicketIdGenKey);
}
if (serviceTicketUniqueTicketIdGenerator == null) {
// 若Service不存在特定的UniqueTicketIdGenerator,则使用默认
serviceTicketUniqueTicketIdGenerator = this.defaultServiceTicketIdGenerator;
logger.debug("Service ticket id generator not found for [{}]. Using the default generator...",
uniqueTicketIdGenKey);
}
// 生成ST ID
final String ticketId = serviceTicketUniqueTicketIdGenerator.getNewTicketId(ServiceTicket.PREFIX);
// 借助TGT创建ST
final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket(
ticketId,
service,
this.serviceTicketExpirationPolicy,
credentialsProvided,
this.onlyTrackMostRecentSession);
return (T) serviceTicket;
}
TGT是TicketGrantingTicketImpl
类型的,其grantServiceTicket
方法的代码如下:
public final synchronized ServiceTicket grantServiceTicket(final String id,
final Service service, final ExpirationPolicy expirationPolicy,
final boolean credentialsProvided, final boolean onlyTrackMostRecentSession) {
// 创建ST
final ServiceTicket serviceTicket = new ServiceTicketImpl(id, this,
service, this.getCountOfUses() == 0 || credentialsProvided,
expirationPolicy);
//
updateServiceAndTrackSession(serviceTicket.getId(), service, onlyTrackMostRecentSession);
return serviceTicket;
}
protected void updateServiceAndTrackSession(final String id, final Service service, final boolean onlyTrackMostRecentSession) {
updateState();
// 更新服务Principal信息
final List<Authentication> authentications = getChainedAuthentications();
service.setPrincipal(authentications.get(authentications.size()-1).getPrincipal());
// 如果开启跟踪最近访问会话,则需要清除缓存的该service信息若存在
if (onlyTrackMostRecentSession) {
final String path = normalizePath(service);
final Collection<Service> existingServices = services.values();
// loop on existing services
for (final Service existingService : existingServices) {
final String existingPath = normalizePath(existingService);
// if an existing service has the same normalized path, remove it
// and its service ticket to keep the latest one
if (StringUtils.equals(path, existingPath)) {
// 存在该service的缓存信息则需要清除
existingServices.remove(existingService);
LOGGER.trace("Removed previous tickets for service: {}", existingService);
break;
}
}
}
// 缓存该service的信息
this.services.put(id, service);
}
protected final void updateState() {
// 更新记录
this.previousLastTimeUsed = this.lastTimeUsed;
this.lastTimeUsed = System.currentTimeMillis();
this.countOfUses++;
}
至此,创建ST的流程结束;
成功创建出ST后,就会走success
处理,即warn
,其配置如下:
<!-- login-webflow.xml -->
<decision-state id="warn">
<if test="flowScope.warnCookieValue" then="showWarningView" else="redirect"/>
</decision-state>
根据表达式可知,若无warn信息,则走redirect
,其配置如下:
<!-- login-webflow.xml -->
<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>
根据表达式可知,根据service获取带有ST ID信息Response,代码如下:
// AbstractWebApplicationService.java
public Response getResponse(final String ticketId) {
return this.responseBuilder.build(this, ticketId);
}
// WebApplicationServiceResponseBuilder.java
public Response build(final WebApplicationService service, final String ticketId) {
final Map<String, String> parameters = new HashMap<>();
if (StringUtils.hasText(ticketId)) {
// 保存ST ID信息
parameters.put(CasProtocolConstants.PARAMETER_TICKET, ticketId);
}
if (responseType.equals(Response.ResponseType.POST)) {
return buildPost(service, parameters);
}
if (responseType.equals(Response.ResponseType.REDIRECT)) {
return buildRedirect(service, parameters);
}
throw new IllegalArgumentException("Response type is valid. Only POST/REDIRECT are supported");
}
创建出Response后,走postRedirectDecision
,其配置如下:
<!-- login-webflow.xml -->
<decision-state id="postRedirectDecision">
<if test="requestScope.response.responseType.name() == 'POST'" then="postView" else="redirectView"/>
</decision-state>
根据表达式可知,若响应时POST类型,则走postView
,否则走redirectView
,本例走redirectView
,其配置如下:
<!-- login-webflow.xml -->
<end-state id="redirectView" view="externalRedirect:#{requestScope.response.url}"/>
根据表达式可知,通过ExternalRedirectAction
实现重定向,ExternalRedirectAction
继承自AbstractAction
,实现了算法细节doExecute
,代码如下:
protected Event doExecute(RequestContext context) throws Exception {
String resourceUri = (String)this.resourceUri.getValue(context);
context.getExternalContext().requestExternalRedirect(resourceUri);
return this.success();
}
重定向到login请求中携带的Service
,并携带新创建的ST信息;
至此,携带Service
的login请求的处理流程结束。
最后
以上就是紧张小海豚为你收集整理的CAS流程简析 服务端处理携带Service登录请求相关阅读login请求的处理流程配置login请求的处理流程开启login请求的处理流程简析的全部内容,希望文章能够帮你解决CAS流程简析 服务端处理携带Service登录请求相关阅读login请求的处理流程配置login请求的处理流程开启login请求的处理流程简析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复