keycloak~分布式部署中会话过期清理机制

Keycloak 分布式部署中会话过期清理机制

在 Keycloak 分布式部署(使用外部独立部署的 Infinispan)的架构下,sessionsclientSessions 的过期清理涉及两种不同的部署模式,机制略有不同:

架构模式 1:Embedded + Remote Store(嵌入式缓存 + 远程存储)

这种模式下,Keycloak 节点有本地嵌入式缓存,同时配置了远程存储(Remote Store)连接到外部 Infinispan 集群。

/**
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
@ClientListener
public class RemoteCacheSessionListener<K, V extends SessionEntity>  {

清理机制:

  1. 本地缓存自动过期
    • 当会话数据写入本地嵌入式缓存(DefaultSegmentedDataContainer)时,会同时设置 lifespanmaxIdle 参数
    • Infinispan 的内置过期机制会自动清除过期条目

图片

  1. 远程缓存事件同步
    • RemoteCacheSessionListener 通过 Hot Rod Client Listener 机制监听远程缓存事件
    • 当远程 Infinispan 中条目被删除时,会触发 @ClientCacheEntryRemoved 事件:
    @ClientCacheEntryRemoved
    public void removed(ClientCacheEntryRemovedEvent event) {
        K key = (K) event.getKey();

        if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {

            this.executor.submit(event, () -> {

                // We received event from remoteCache, so we won't update it back
                cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
                        .remove(key);

            });
        }
    }
  1. 重要限制
    • Infinispan 不发送过期事件(Expiration Event)给 Hot Rod 客户端监听器!
    • 远程 Infinispan 中的条目过期时,不会主动通知 Keycloak 节点
    • 本地缓存的清理完全依赖于本地 Infinispan 的自动过期机制

架构模式 2:Remote Only(纯远程模式)

这种模式下,Keycloak 不维护本地会话缓存,所有会话数据都直接存储在外部 Infinispan 集群中。

    @Override
    public void removeAllExpired() {
        //rely on Infinispan expiration
    }

    @Override
    public void removeExpired(RealmModel realm) {
        //rely on Infinispan expiration
    }

清理机制

  • 完全依赖远程 Infinispan 的过期机制
  • Keycloak 本地 JVM 中没有 DefaultSegmentedDataContainer,因为不使用嵌入式缓存
  • 所有读取操作直接访问远程缓存,过期数据自然不会被读取到

关键代码:过期时间计算

无论哪种模式,会话的过期时间都通过 SessionTimeouts 计算:

    public static long getUserSessionLifespanMs(RealmModel realm, ClientModel client, UserSessionEntity userSessionEntity) {
        return getUserSessionLifespanMs(realm, false, userSessionEntity.isRememberMe(), userSessionEntity.getStarted());
    }

    public static long getUserSessionLifespanMs(RealmModel realm, boolean offline, boolean rememberMe, int started) {
        long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(offline, rememberMe,
                TimeUnit.SECONDS.toMillis(started), realm);
        if (offline && lifespan == IMMORTAL_FLAG) {
            return IMMORTAL_FLAG;
        }
        lifespan = lifespan - Time.currentTimeMillis();
        if (lifespan <= 0) {
            return ENTRY_EXPIRED_FLAG;
        }
        return lifespan;
    }

总结:本地 JVM 中 DefaultSegmentedDataContainer 对象的清理方式

场景 清理机制
本地条目自然过期 Infinispan 嵌入式缓存的内置过期 Reaper 线程自动清理
远程条目被删除 通过 RemoteCacheSessionListener@ClientCacheEntryRemoved 事件同步删除本地条目
远程条目自然过期 不会主动通知!本地条目依赖自身的过期时间自动失效
Failover 事件 触发 onFailover 回调,清空整个本地缓存(ispnCache::clear)以保证一致性

潜在问题

由于远程 Infinispan 的过期事件不会通知本地缓存,在以下情况下可能存在短暂的数据不一致

  1. 远程条目已过期被清除
  2. 但本地缓存的过期时间还没到
  3. 此时本地可能返回一个"幽灵会话"

Keycloak 的解决方案

  • 在每次从本地缓存获取会话时,都会检查 Expiration.isExpired()
  • 如果计算出的过期时间已经过了,即使缓存条目存在也会被视为无效
    public boolean isExpired() {
        return maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG;
    }
/**
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
@ClientListener
public class RemoteCacheSessionListener<K, V extends SessionEntity>  {
    @ClientCacheEntryRemoved
    public void removed(ClientCacheEntryRemovedEvent event) {
        K key = (K) event.getKey();

        if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {

            this.executor.submit(event, () -> {

                // We received event from remoteCache, so we won't update it back
                cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
                        .remove(key);

            });
        }
    }
    @Override
    public void removeAllExpired() {
        //rely on Infinispan expiration
    }

    @Override
    public void removeExpired(RealmModel realm) {
        //rely on Infinispan expiration
    }
    public static long getUserSessionLifespanMs(RealmModel realm, ClientModel client, UserSessionEntity userSessionEntity) {
        return getUserSessionLifespanMs(realm, false, userSessionEntity.isRememberMe(), userSessionEntity.getStarted());
    }

    public static long getUserSessionLifespanMs(RealmModel realm, boolean offline, boolean rememberMe, int started) {
        long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(offline, rememberMe,
                TimeUnit.SECONDS.toMillis(started), realm);
        if (offline && lifespan == IMMORTAL_FLAG) {
            return IMMORTAL_FLAG;
        }
        lifespan = lifespan - Time.currentTimeMillis();
        if (lifespan <= 0) {
            return ENTRY_EXPIRED_FLAG;
        }
        return lifespan;
    }
    public boolean isExpired() {
        return maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG;
    }
posted @ 2026-01-30 13:19  张占岭  阅读(6)  评论(0)    收藏  举报