本文主要讲解ticket的失效判断策略,源码目前还是以开源cas4.2.7版本为主。
1、ticket失效策略的默认使用类
ticket的失效可分为两类一个是TGT(TicketGrantingTicket)的失效策略,另外一个是ST(ServiceTicket)的失效策略。结合源码我们可知在创建ticket的时候就必须放入对应的失效策略,针对不同的ticket会放入不同的失效策略,先看看具体有多少种失效策略。
具体的失效策略主要是实现ExpirationPolicy这个接口类来完成,然后基于实现的抽象类AbstractCasExpirationPolicy来进行具体的实现。
public interface ExpirationPolicy extends Serializable {
boolean isExpired(TicketState ticketState);
}
基于AbstractCasExpirationPolicy类的实现有8个类,这些类都在cas-server-core-tickets的org.jasig.cas.ticket.support包下,具体为
类名 | 说明 |
---|---|
AlwaysExpiresExpirationPolicy | 总是失效策略 |
HardTimeoutExpirationPolicy | 超时时间由票据创建时间决定与当前时间的间隔策略 |
MultiTimeUseOrTimeoutExpirationPolicy | 基于票据的特定使用次数或票据上次使用时间与现在存在的特定时间段的过期策略 |
NeverExpiresExpirationPolicy | 永不失效策略 |
RememberMeDelegatingExpirationPolicy | 根据“记住我”的结果,委托不同的过期策略 |
ThrottledUseAndTimeoutExpirationPolicy | |
TicketGrantingTicketExpirationPolicy | 票据具有固定的生存期和空闲使用超时时间 |
TimeoutExpirationPolicy | 基于票据存在的特定时间段的过期策略 |
依托于这些失效策略,在CAS单点登录过程中会实时的校验对应的ticket状态。那么来看看单点登录过程中最关注的TGT和ST的默认失效策略采用的是哪种方式。在CAS4.2.7版本中的\webapp\WEB-INF\deployerConfigContext.xml文件中有如下的配置:
<alias name="ticketGrantingTicketExpirationPolicy" alias="grantingTicketExpirationPolicy" />
<alias name="multiTimeUseOrTimeoutExpirationPolicy" alias="serviceTicketExpirationPolicy" />
2、TGT失效策略分析
2.1、TGT创建解析
通过配置可知TGT的失效策略是采用TicketGrantingTicketExpirationPolicy来实现。如何在创建TGT的时候设定策略的呢,我们可以看一下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);
****
return ticketGrantingTicket;
}
通过上面代码可知在创建TGT的时候是由工厂类依据认证信息来创建,那么继续看工厂类中是如何引入TGT的失效策略。TGT创建的工厂类具体实现为DefaultTicketGrantingTicketFactory在cas-server-core-tickets的org.jasig.cas.ticket包下,来看具体的实现方法。
DefaultTicketGrantingTicketFactory.java
@NotNull
@Resource(name="ticketGrantingTicketUniqueIdGenerator")
protected UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator;
@NotNull
@Resource(name="grantingTicketExpirationPolicy")
protected ExpirationPolicy ticketGrantingTicketExpirationPolicy;
@Override
public <T extends TicketGrantingTicket> T create(final Authentication authentication) {
//创建TGT时,传入了ticketGrantingTicketExpirationPolicy策略
final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(TicketGrantingTicket.PREFIX),
authentication, ticketGrantingTicketExpirationPolicy);
return (T) ticketGrantingTicket;
}
从上面的代码可以看到在创建TGT的时候,根据注入的ticketGrantingTicketExpirationPolicy构造出TGT对象,那么我们继续分析TGT默认失效策略是什么样的。通过注入对象可知具体的实现类就是为TicketGrantingTicketExpirationPolicy,分析一下具体的失效方法
@Override
public boolean isExpired(final TicketState ticketState) {
final long currentSystemTimeInMillis = System.currentTimeMillis();
//校验tgt的最大存活时间,根据tgt的创建时间和当前时间的差值来判断
// Ticket has been used, check maxTimeToLive (hard window)
if (currentSystemTimeInMillis - ticketState.getCreationTime() >= maxTimeToLiveInMilliSeconds) {
LOGGER.debug("Ticket is expired because the time since creation is greater than maxTimeToLiveInMilliSeconds");
return true;
}
//校验tgt的最大使用空闲时间,根据tgt上次使用时间和当前时间的差值来判断
// Ticket is within hard window, check timeToKill (sliding window)
if (currentSystemTimeInMillis - ticketState.getLastTimeUsed() >= timeToKillInMilliSeconds) {
LOGGER.debug("Ticket is expired because the time since last use is greater than timeToKillInMilliseconds");
return true;
}
return false;
}
根据失效方法判断可知tgt有两个判断失效的时间配置:maxTimeToLiveInMilliSeconds最大生存时间;timeToKillInMilliSeconds最大空闲使用时间。
2.2、TGT失效判定解析
通过上文解析TGT的创建过程,知晓了TGT创建中会设定的失效策略,那么TGT在何时会去校验失效呢?基于CAS4.2.7版本中我们可以通过登录的流程中可以看到是在TicketGrantingTicketCheckAction类上进行了判断,此类在cas-server-webapp-actions的org.jasig.cas.web.flow包下,具体代码如下
protected Event doExecute(final RequestContext requestContext) throws Exception {
final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
if (!StringUtils.hasText(tgtId)) {
return new Event(this, NOT_EXISTS);
}
String eventId = INVALID;
try {
final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
//判断TGT是否存在并有效未过期
if (ticket != null && !ticket.isExpired()) {
eventId = VALID;
}
} catch (final AbstractTicketException e) {
logger.trace("Could not retrieve ticket id {} from registry.", e);
}
return new Event(this, eventId);
}
因此在每次使用登录成功之后,使用TGT的时候都会校验是否有效,如果TGT已经过期那么整个流程就将失效,回到CAS单点登录的登录页面。
3、ST失效策略分析
通过上文解析了TGT的失效策略之后,继续看看ST的失效策略是如何配置和执行的。
3.1、ST创建解析
通过配置可知ST的失效策略是采用MultiTimeUseOrTimeoutExpirationPolicy来实现。那么如何在创建ST的时候设定策略的呢,我们可以看一下CentralAuthenticationServiceImpl这个类中是如何通过已有的TGT来创建出ST的方法:
public ServiceTicket grantServiceTicket(
final String ticketGrantingTicketId,
final Service service, final AuthenticationContext context)
throws AuthenticationException, AbstractTicketException {
logger.debug("Attempting to get ticket id {} to create service ticket", ticketGrantingTicketId);
final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
*******
//根据ST类类型创建ST的工厂类
final ServiceTicketFactory factory = this.ticketFactory.get(ServiceTicket.class);
//由工厂类依托TGT和认证信息创建ST
final ServiceTicket serviceTicket = factory.create(ticketGrantingTicket, service,
context != null && context.isCredentialProvided());
logger.info("Granted ticket [{}] for service [{}] and principal [{}]",
serviceTicket.getId(), service.getId(), principal.getId());
//TGT中关联ST
this.ticketRegistry.addTicket(serviceTicket);
logger.debug("Added service ticket {} to ticket registry", serviceTicket.getId());
******
return serviceTicket;
}
通过上面代码可知在创建ST的时候是由工厂类依据TGT和认证信息来创建,那么继续看工厂类中是如何引入ST的失效策略。ST创建的工厂类具体实现为DefaultServiceTicketFactory在cas-server-core-tickets的org.jasig.cas.ticket包下,来看具体的实现方法。
public <T extends Ticket> T create(final TicketGrantingTicket ticketGrantingTicket,
final Service service,
final boolean credentialsProvided) {
final String uniqueTicketIdGenKey = service.getClass().getName();
UniqueTicketIdGenerator serviceTicketUniqueTicketIdGenerator = null;
if (this.uniqueTicketIdGeneratorsForService != null && !uniqueTicketIdGeneratorsForService.isEmpty()) {
logger.debug("Looking up service ticket id generator for [{}]", uniqueTicketIdGenKey);
serviceTicketUniqueTicketIdGenerator = this.uniqueTicketIdGeneratorsForService.get(uniqueTicketIdGenKey);
}
*****
final String ticketId = serviceTicketUniqueTicketIdGenerator.getNewTicketId(ServiceTicket.PREFIX);
//根据tgtid,service和serviceTicketExpirationPolicy策略来进行创建ST
final ServiceTicket serviceTicket = ticketGrantingTicket.grantServiceTicket(
ticketId,
service,
this.serviceTicketExpirationPolicy,
credentialsProvided,
this.onlyTrackMostRecentSession);
return (T) serviceTicket;
}
从上面的代码可以看到在创建ST的时候,根据注入的serviceTicketExpirationPolicy构造出ST对象,那么我们继续分析ST默认失效策略是什么样的。通过注入对象可知具体的实现类就是为MultiTimeUseOrTimeoutExpirationPolicy,分析一下ST具体的失效方法。
public boolean isExpired(final TicketState ticketState) {
if (ticketState == null) {
LOGGER.debug("Ticket state is null for {}", this.getClass().getSimpleName());
return true;
}
//基于st的使用次数进行判定st是否失效
final long countUses = ticketState.getCountOfUses();
if (countUses >= this.numberOfUses) {
LOGGER.debug("Ticket usage count {} is greater than or equal to {}", countUses, this.numberOfUses);
return true;
}
final long systemTime = System.currentTimeMillis();
final long lastTimeUsed = ticketState.getLastTimeUsed();
final long difference = systemTime - lastTimeUsed;
//校验st的最大使用空闲时间,根据st上次使用时间和当前时间的差值来判断
if (difference >= this.timeToKillInMilliSeconds) {
LOGGER.debug("Ticket has expired because the difference between current time [{}] "
+ "and ticket time [{}] is greater than or equal to [{}]", systemTime, lastTimeUsed,
this.timeToKillInMilliSeconds);
return true;
}
return false;
}
根据失效方法判断可知ST的失效主要是ST的使用次数和最大空闲时间。
3.2、ST失效判定解析
ST又是在何时解析并进行有效性验证的呢?具体可以参考本人的文章《CAS单点登录开源框架解读(九)–CAS单点登录客户端认证之服务端验证票据返回认证信息》。具体的验证方法如下所示:
AbstractServiceValidateController.java
protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
********
//根据st进行校验并返回认证结果信息
final Assertion assertion = this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
*****
}
CentralAuthenticationServiceImpl.java
public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws AbstractTicketException {
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
verifyRegisteredServiceProperties(registeredService, service);
final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);
********
try {
//同步ST并校验ST是否失效,那么这里使用的策略就是创建ST时候注入的策略,如果失效就直接抛出异常并响应给客户端
synchronized (serviceTicket) {
if (serviceTicket.isExpired()) {
logger.info("ServiceTicket [{}] has expired.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
if (!serviceTicket.isValidFor(service)) {
logger.error("Service ticket [{}] with service [{}] does not match supplied service [{}]",
serviceTicketId, serviceTicket.getService().getId(), service);
throw new UnrecognizableServiceForServiceTicketValidationException(serviceTicket.getService());
}
*********
}
}
由于ST的校验是客户端发起请求,因此校验是在controller中进行实现也会根据具体的异常反馈给客户端。
4、TGT失效问题注意点
在后续CAS新版本中由于TGT对象会进行数据库或者redis进行持久化,针对这类存储中的超时时间的设定同时,也要考虑TGT失效策略的时间配置。按照TGT默认的失效策略中,在CAS的配置文件中有两个对应的属性tgt.maxTimeToLiveInSeconds和tgt.timeToKillInSeconds。如果tgt.timeToKillInSeconds这个使用的空闲时间小于tgt.maxTimeToLiveInSeconds的配置时间,那么很可能你持久化的TGT数据还没有失效,但是在程序中TGT对象基于失效策略已经判定过期,那么很可能出现登录页面或者响应401的情况出现。