【CAS6.6源码解析】深度解析默认票据存储策略及其拓展支持-探究存储策略的设计

CAS作为一款企业级中央认证服务系统,其票据的生成是非常重要的一环,在票据的生成中,还有一个比较重要的点是票据的存储,本文将默认票据存储策略及其拓展支持,并延伸到探究存储策略的设计。
文章重点分析源码的过程,不想看分析过程可以直接跳到总结处看结论!!!


文章目录


A.相关阅读

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 默认票据存储实例

DefaultCentralAuthenticationServicecreateTicketGrantingTicket中,创建了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

查看其源码:

java 复制代码
@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中。

java 复制代码
@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的入口在DefaultCentralAuthenticationServicegrantServiceTicket方法中,核心代码如下:

首先拿到ST的factory,通过factory创建ST,然后更新TGT,最后将ST进行存储。

其最终授予ST的核心代码如下:是通过ticketGrantingTicketgrantServiceTicket进行授予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将无法成功启动!


C.总结

  • CAS6.6中通过默认存储策略的@ConditionalOnMissingBean(name = TicketRegistry.BEAN_NAME)注解和拓展支持类中的@Bean实现了默认票据存储策略及其它拓展票据存储策略的支持。
  • DefaultTicketRegistry本质是将票据存储在ConcurrentHashMap中,将其初始容量,并发数,加密器拓展成了配置,并有默认配置。
  • DefaultTicketRegistry进行了分层设计,从顶级抽象类到该类,每个中间类都只是完成它管辖范围内的操作,其余操作交由其子类来具体实现。是一种经典的模版方法模式。
  • ST和TGT进行了关联,在删除TGT的时候,同时会删除ST。
  • 若需要新增一种存储策略,只需要依赖新模块后,用@Bean注解将Ticketregistry的新实现类注入到容器中,即可完成拓展。

D.展望

本文只着重分析了TicketRegistry下默认的票据存储策略和拓展支持的分析,对票据过期策略和票据清理策略等的设计还未分析,预计会在未来详细分析这些模块。

E.探究存储策略的设计

参考CAS的思路,为某种数据设计存储策略时,若要保障足够的拓展性,可以从以下几个方面进行考虑:

  • 将存储的过程进行详细的拆分,设计多级接口多级抽象类,每个类完成指定范围内的工作,剩下的操作使用模版方法模式拓展给子类进行实现。
  • 将凡是可能变的参数配置在配置文件中,并提供默认配置,这样能通过配置文件完成高度的拓展。
  • 通过使用@ConditionalOnMissingBean注解的方式为顶级接口注入默认的实现类,若要拓展出一种其他的存储策略,只需要实现顶级接口,并使用@Bean注入容器中,即可实现。

参考

截止2023-07-31为止,还没有专门分析CAS6源码的文章可检索,本文只参考了CAS6.6的源代码,所有分析过程均经过动态调试验证。


ATFWUS 2023-07-31

相关推荐
王小磊学代码11 天前
每天一学(2)
线程池·cas·阻塞队列
他叫阿来1 个月前
CAS操作
cas·原子性·aba问题
慕木兮人可1 个月前
SpringBoot2.0.x旧版集成Swagger UI报错Unable to infer base url...解决办法
java·spring boot·cas·springsecurity·swagger-ui
Hello-Brand3 个月前
高并发下的数据一致性保障(图文全面总结)
分布式·高并发·分布式锁·cas·一致性·aba
星月IWJ4 个月前
cas_ssl
cas
八了个戒5 个月前
单点登陆(SSO)基于CAS实现前后端分离的SSO系统开发「IDP发起」
前端·javascript·cas·大前端·sso
若明天不见6 个月前
【多线程与高并发 四】CAS、Unsafe 及 JUC 原子类详解
java·cas·juc·unsafe·atomic原子类
Hello-Brand7 个月前
Java核心知识体系8:Java如何保证线程安全性
java·cas·并发编程·线程安全性·synchronized·nocas·volatile·final·多线程模型·case
玛卡巴咖9 个月前
CAS详解和学透面试必问并发安全问题
职场和发展·并发·cas
不能再留遗憾了9 个月前
【JavaEE】CAS(Compare And Swap)操作
java·java-ee·cas