分布式系统下 JWT 公钥缓存怎么防止多节点加载不一致?

文章导读
采用中心化存储 + 分布式锁方案可将公钥刷新冲突降低 90% 以上,建议提前 10-20 分钟缓冲刷新避免多节点同时请求导致的服务中断。
📋 目录
  1. 原因分析
  2. 解决方案
  3. 注意事项
  4. 参考来源
A A

分布式系统下 JWT 公钥缓存怎么防止多节点加载不一致?

核心结论:采用中心化存储 + 分布式锁方案可将公钥刷新冲突降低 90% 以上,建议提前 10-20 分钟缓冲刷新避免多节点同时请求导致的服务中断。

原因分析

在分布式系统中,多个应用节点同时持有 JWT 公钥缓存副本,当密钥轮换时若各节点缓存失效时间不一致,会导致部分节点验证失败。典型场景包括:用户信息在 A 节点缓存中为旧值,而 B 节点已更新至新值;缓存失效策略(如 TTL)无法保证所有节点同时过期。根据 2025 年 10 月 21 日 CSDN 博客《分布式环境下缓存一致性挑战》分析,缓存一致性问题的根本原因是服务实例横向扩展后,多个节点同时持有同一份数据副本,数据更新时缓存未能及时同步。

JWT 公钥缓存的特殊性在于:公钥通常从远程 JWKS 端点获取(如 https://example.com/.well-known/jwks.json),网络请求和解析开销较大,若每个节点独立刷新会产生大量重复请求。企业微信 access_token 案例显示,每日获取次数通常限制为 1000 次,若多个节点同时刷新会导致整个集群因"Token Invalid"而集体挂掉。

解决方案

方案一:中心化存储 + 分布式锁(推荐)

将公钥及其过期时间存入 Redis,利用分布式锁确保全局只有一个节点执行刷新动作。参考 2026 年 1 月 23 日发布的《Java 分布式环境下的 Access_Token 一致性方案》实现:

@Service
public class JwtKeyService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String KEY_KEY = "jwt:public_key";
    private static final String LOCK_KEY = "jwt:key_lock";
    
    public String getSafePublicKey() {
        String key = redisTemplate.opsForValue().get(KEY_KEY);
        if (StringUtils.isEmpty(key) || isKeyExpiringSoon()) {
            RLock lock = redissonClient.getLock(LOCK_KEY);
            try {
                // 竞争分布式锁,最多等待 10 秒,锁定时间 30 秒
                if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
                    // 二次检查,防止等待锁期间另一个节点已刷新
                    key = redisTemplate.opsForValue().get(KEY_KEY);
                    if (StringUtils.isEmpty(key)) {
                        key = refreshKeyFromJwks();
                        // 存入 Redis,设置比官方稍短的过期时间(例如 7000 秒)
                        redisTemplate.opsForValue().set(KEY_KEY, key, 7000, TimeUnit.SECONDS);
                    }
                }
            } finally {
                lock.unlock();
            }
        }
        return key;
    }
}

关键参数:分布式锁等待时间 10 秒,锁定时间 30 秒,缓存过期时间 7000 秒(比官方 7200 秒稍短,预留缓冲时间)。

方案二:使用缓存中间件内置一致性机制

php-jwt 库提供了 CachedKeySet 类(src/CachedKeySet.php),可缓存从远程获取的密钥集,减少网络请求和解析开销。根据 2026 年 2 月 5 日《超实用指南:php-jwt 高并发处理与分布式验证优化》:

use Firebase\JWT\CachedKeySet;
$jwksUri = 'https://example.com/.well-known/jwks.json';
$cache = new MyCacheImplementation(); // 实现你自己的缓存逻辑
$keySet = new CachedKeySet($jwksUri, $cache, 3600); // 缓存 1 小时
$decoded = JWT::decode($jwt, $keySet);

缓存时间设置为 3600 秒(1 小时),需配合外部缓存系统(如 Redis)实现跨节点共享。

方案三:基于消息队列的缓存同步

使用消息中间件(如 Kafka)广播公钥失效事件,各节点监听并主动清除本地缓存。参考 2025 年 10 月 21 日 CSDN 博客示例:

// 发布缓存失效消息
func publishInvalidateEvent(key string) {
    event := map[string]string{
        "action": "invalidate",
        "key": key,
    }
    // 将事件发送至 Kafka 主题 cache-invalidation
    kafkaProducer.Send("cache-invalidation", event)
}

// 消费端处理失效事件
func consumeInvalidateEvent() {
    for event := range kafkaConsumer.Ch {
        if event.Action == "invalidate" {
            localCache.Delete(event.Key) // 删除本地缓存
        }
    }
}

此方案适用于节点数量较多(10+)的场景,但会增加系统复杂度。

注意事项

1. 提前刷新策略:不要等到公钥完全过期再刷新,建议提前 10-20 分钟(Buffer Time)。根据 2026 年 1 月 23 日资料,企业微信 access_token 案例中提前刷新可避免多节点同时刷新导致的"Token Invalid"错误。

分布式系统下 JWT 公钥缓存怎么防止多节点加载不一致?

2. 二次检查机制:抢到分布式锁的节点在刷新前需再次检查缓存,防止等待锁期间另一个节点已刷新完成。这是避免重复请求的关键步骤。

3. 密钥算法选择:生产环境优先使用 RS256 等非对称算法,密钥长度至少 32 字节。根据 2026 年 4 月 17 日《JWT 实战中的关键问题与解决方案》,对称加密密钥(HS256)如果泄露,攻击者可伪造任意 Token。

4. 缓存过期时间设置:存入缓存的过期时间应比官方过期时间稍短(如官方 7200 秒,缓存设 7000 秒),预留缓冲时间避免边界情况。

5. 本地缓存风险:单机服务下使用本地 Map 存储不实用,仅适合存储本次请求中的一次性信息(如 ThreadLocal)。根据 2023 年 12 月 13 日资料,本地缓存无法解决分布式一致性问题。

参考来源

来源:CSDN 博客 - 分布式环境下缓存一致性挑战(2025 年 10 月 21 日)

来源:Java 分布式环境下的 Access_Token 一致性方案(2026 年 1 月 23 日)

来源:超实用指南:php-jwt 高并发处理与分布式验证优化(2026 年 2 月 5 日)

来源:JWT 实战中的关键问题与解决方案(2026 年 4 月 17 日)