CAS作为一款企业级中央认证服务系统,其票据的生成是非常重要的一环,在票据的生成中,还有一个比较重要的点是票据的存储,本文将默认票据存储策略及其拓展支持,并延伸到探究存储策略的设计。
文章重点分析源码的过程,不想看分析过程可以直接跳到总结处看结论!!!
文章目录
- A.相关阅读
- B.涉及源码作用及位置介绍
- 1.票据存储策略核心源码
- 2.拓展存储策略支持
- C.默认存储策略深入解析
- 1.入口-存储票据
- 1.1 默认票据存储实例
- 1.2 默认票据存储策略配置
- 2.DefaultTicketRegistry分析
- 2.1 类关系图
- 2.2 DefaultTicketRegistry
- 2.3 AbstractMapBasedTicketRegistry
- 2.4 AbstractTicketRegistry
- 2.5 涉及设计模式
- 2.6 小结
- 3.ST存储策略以及和TGT的关系
- 3.1 ST创建链路
- 3.2 ST存储
- 4.如何做到票据存储策略的模块化拓展
- 总结
- 展望
- 探究存储策略的设计
- 参考
A.相关阅读
- 【CAS6.6源码解析】在IDEA中调试可插拔的supprot模块
- 【CAS6.6源码解析】调试Rest API接口
- 【CAS6.6源码解析】深入解析TGT和ST的唯一ID是怎样生成的-探究ID生成器的设计
B.涉及源码作用及位置介绍
CAS中,默认支持的是内存的存储策略,涉及存储策略的核心代模块默认会被依赖,但是一些拓展支持的模块如redis存储等属于support模块,需要添加依赖后才会生效。
1.票据存储策略核心源码
1.票据存储策略相关顶级接口在cas-servver-core-api-ticket
中的registry
包下:
TicketRegistry
是票据存储的顶级接口,里面规范了一种存储策略需要实现的方法。TicketRegistryCleaner
是票据清理的顶级接口,里面规范了一种票据清理器需要实现的方法。TicketRegistrySupport
是一个帮助者模式的顶级接口,里面定义了一些需要相互共享和使用的互相不相关的方法。
2.上述接口的默认实现类,在cas-server-core-tickets-api
模块下的registry
包下。
2.拓展存储策略支持
所有支持的存储策略模块均在support
模块下,模块名以-ticket-registry
结尾。例如redis支持:
CAS6.6支持的存储策略有:(13种存储方式)
- redis
- couchbase(Couchbase是一个开源的分布式NoSQL文档数据库)
- couchdb(CouchDB 是一个开源的面向文档的数据库管理系统)
- dynamodb(AmazonDynamoDB被设计成用来托管的NoSQL数据库服务、可预期的性能、可实现无缝扩展性和可靠性等核心问题)
- ehcache3(Ehcache 3 是一个强大的缓存技术,它提供了分布式缓存和本地缓存两种模式,并且支持缓存的大小控制、缓存的预热、缓存存储选项和缓存的管理等功能)
- ehcache(EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider)
- hazelcast(Hazelcast是一个高度可扩展的数据分发和集群平台,可用于实现分布式数据存储、数据缓存)
- ignite(追求高性能/高吞吐量和线性扩展能力 关系型数据库缓存)
- infinispan(一个分布式集群缓存系统)
- JMS(Java Message Service)
- JPA
- memcached(memcached是一套分布式的高速缓存系统)
- mongodb(基于分布式文件存储的数据库)
C.默认存储策略深入解析
这里以TGT票据的存储为例展开解析,ST与TGT保持一致。
1.入口-存储票据
1.1 默认票据存储实例
在DefaultCentralAuthenticationService
的createTicketGrantingTicket
中,创建了TGT后,会通过configurationContext
拿到TicketRegistry
的一个实例,并且将票据进行存储,如下图:
1.2 默认票据存储策略配置
查看configurationContext
的配置可以发现,默认注入的TicketRegistry
的实现类是DefaultTicketRegistry
。
这里有个很关键的点:当TicketRegistry
没有实现类时,才会去注入DefaultTicketRegistry
,这是完成票据存储拓展支持的核心点。
并且可以看到传入的参数是一个ConcurrentHashMap,其中初始容量、并发数和加密器是在配置文件中进行配置的。查看默认配置:
默认配置里,初始容量是1000,并发数是20,并且默认是不开启加密的。
2.DefaultTicketRegistry分析
从上述入口可以看出默认使用的是DefaultTicketRegistry
实现类,并且知道了默认配置参数。接下来就是仔细分析DefaultTicketRegistry
这个类的实现了。
2.1 类关系图
类关系图如下:
可以发现DefaultTicketRegistry
继承了AbstractMapBasedTicketRegistry
,是一个Map型的存储模式。
2.2 DefaultTicketRegistry
查看其源码:
@Getter
public class DefaultTicketRegistry extends AbstractMapBasedTicketRegistry {
/**
* A map to contain the tickets.
*/
private final Map<String, Ticket> mapInstance;
public DefaultTicketRegistry() {
this(CipherExecutor.noOp());
}
public DefaultTicketRegistry(final CipherExecutor cipherExecutor) {
super(cipherExecutor);
this.mapInstance = new ConcurrentHashMap<>();
}
public DefaultTicketRegistry(final Map<String, Ticket> storageMap, final CipherExecutor cipherExecutor) {
super(cipherExecutor);
this.mapInstance = storageMap;
}
}
主要就是将配置好的ConcurrentHashMap传给父类AbstractMapBasedTicketRegistry
。
2.3 AbstractMapBasedTicketRegistry
基于Map进行存储的逻辑在AbstractMapBasedTicketRegistry
中。
@Slf4j
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class AbstractMapBasedTicketRegistry extends AbstractTicketRegistry {
protected AbstractMapBasedTicketRegistry(final CipherExecutor cipherExecutor) {
setCipherExecutor(cipherExecutor);
}
@Override
public void addTicketInternal(final Ticket ticket) throws Exception {
val encTicket = encodeTicket(ticket);
LOGGER.debug("Putting ticket [{}] in registry.", ticket.getId());
getMapInstance().put(encTicket.getId(), encTicket);
}
@Override
public Ticket getTicket(final String ticketId, final Predicate<Ticket> predicate) {
val encTicketId = encodeTicketId(ticketId);
if (StringUtils.isBlank(ticketId)) {
return null;
}
val found = getMapInstance().get(encTicketId);
if (found == null) {
LOGGER.debug("Ticket [{}] could not be found", encTicketId);
return null;
}
val result = decodeTicket(found);
if (!predicate.test(result)) {
LOGGER.debug("Cannot successfully fetch ticket [{}]", ticketId);
return null;
}
return result;
}
@Override
public long deleteSingleTicket(final String ticketId) {
val encTicketId = encodeTicketId(ticketId);
return !StringUtils.isBlank(encTicketId) && getMapInstance().remove(encTicketId) != null ? 1 : 0;
}
@Override
public long deleteAll() {
val size = getMapInstance().size();
getMapInstance().clear();
return size;
}
@Override
public Collection<? extends Ticket> getTickets() {
return decodeTickets(getMapInstance().values());
}
@Override
public Ticket updateTicket(final Ticket ticket) throws Exception {
LOGGER.trace("Updating ticket [{}] in registry...", ticket.getId());
addTicket(ticket);
return ticket;
}
/**
* Create map instance, which must ben created during initialization phases
* and always be the same instance.
*
* @return the map
*/
public abstract Map<String, Ticket> getMapInstance();
}
梳理一下核心逻辑,可以发现,在此类中实现的方法,仅仅是和票据存储相关的(存储,编码解码),其余票据存储的前后逻辑,仍在其父类AbstractTicketRegistry
中。
2.4 AbstractTicketRegistry
分析其最常用的增删改查方法:(代码过长,只贴部分)
1.存储票据时只校验一下是否过期(过期策略不是本章的重点),具体存储操作交由其子类来处理。
2.获取票据时会依据提供的类进行强转,每个票据获取时还会进行过期校验,如果过期会直接删除。
3.删除票据时,如果是TGT,还会将其授予的ST全部删除。
2.5 涉及设计模式
AbstractTicketRegistry
类采用模版方法模式将具体的存储操作交由子类完成,拓展了存储的多样性。
AbstractMapBasedTicketRegistry
类采用模版方法模式将Map的实例化交由其子类来完成,拓展了Map的多样性。
2.6 小结
DefaultTicketRegistry
本质是将票据存储在ConcurrentHashMap中,将其初始容量,并发数,加密器拓展成了配置,并有默认配置。
DefaultTicketRegistry
进行了分层设计,从顶级抽象类到该类,每个中间类都只是完成它管辖范围内的操作,其余操作交由其子类来具体实现。
3.ST存储策略以及和TGT的关系
上述是TGT的默认存储策略,我们来看一下ST是如何存储的。
3.1 ST创建链路
授予ST的入口在DefaultCentralAuthenticationService
的grantServiceTicket
方法中,核心代码如下:
首先拿到ST的factory,通过factory创建ST,然后更新TGT,最后将ST进行存储。
其最终授予ST的核心代码如下:是通过ticketGrantingTicket
的grantServiceTicket
进行授予ST。
其中,在新建ST对象的时候,会关联TGT:
同时,在trackingPolicy.track(this, serviceTicket);
中,会将ST关联的TGT的services MAP中。
其余票据生成过程不是本章关心的内容,这里主要分析其存储的关联关系。
总结:ST是由TGT实现类的某个方法授予的,ST在初始化的时候,指定了TGT属性进行关联,ST创建完成后,TGT会将ST加入到一个services的map中进行关联。在删除TGT的时候,也会将其关联的ST全部删除。
3.2 ST存储
存储ST的存储策略仍然是通过configurationContext.getTicketRegistry()
获取的,与TGT完全一致。
4.如何做到票据存储策略的模块化拓展
以redis为例,分析如何拓展支持一种存储策略。
在cas-server-support-redis-ticket-registry
模块的config
包下,注入了RedisTicketRegistry
:
注意此处是直接用的@Bean
申明为一个实体,而在CasCoreTicketsConfiguration
中,如果已经有Ticketregistry
的实体,将不会再注入默认的票据存储策略。
此时Spring容器里面,Ticketregistry
的实现实体就只有RedisTicketRegistry
,那么在通过configurationContext.getTicketRegistry()
获取票据存储策略的时候,得到的就是RedisTicketRegistry
。
注意,拓展支持的存储策略模块中的配置,都是使用@Bean
进行注入的,并未申明对象名字,那么如果同时开启多个存储策略模块,SpringBoot将无法成功启动!
总结
- CAS6.6中通过默认存储策略的
@ConditionalOnMissingBean(name = TicketRegistry.BEAN_NAME)
注解和拓展支持类中的@Bean
实现了默认票据存储策略及其它拓展票据存储策略的支持。 DefaultTicketRegistry
本质是将票据存储在ConcurrentHashMap中,将其初始容量,并发数,加密器拓展成了配置,并有默认配置。DefaultTicketRegistry
进行了分层设计,从顶级抽象类到该类,每个中间类都只是完成它管辖范围内的操作,其余操作交由其子类来具体实现。是一种经典的模版方法模式。- ST和TGT进行了关联,在删除TGT的时候,同时会删除ST。
- 若需要新增一种存储策略,只需要依赖新模块后,用
@Bean
注解将Ticketregistry
的新实现类注入到容器中,即可完成拓展。
展望
本文只着重分析了TicketRegistry
下默认的票据存储策略和拓展支持的分析,对票据过期策略和票据清理策略等的设计还未分析,预计会在未来详细分析这些模块。
探究存储策略的设计
参考CAS的思路,为某种数据设计存储策略时,若要保障足够的拓展性,可以从以下几个方面进行考虑:
- 将存储的过程进行详细的拆分,设计多级接口多级抽象类,每个类完成指定范围内的工作,剩下的操作使用模版方法模式拓展给子类进行实现。
- 将凡是可能变的参数配置在配置文件中,并提供默认配置,这样能通过配置文件完成高度的拓展。
- 通过使用
@ConditionalOnMissingBean
注解的方式为顶级接口注入默认的实现类,若要拓展出一种其他的存储策略,只需要实现顶级接口,并使用@Bean
注入容器中,即可实现。
参考
截止2023-07-31为止,还没有专门分析CAS6源码的文章可检索,本文只参考了CAS6.6的源代码,所有分析过程均经过动态调试验证。