---

title: JWKS公钥轮换与缓存门禁(kid/ttl/回退)最佳实践

keywords:

  • JWKS
  • kid
  • 缓存TTL
  • 回退策略
  • 公钥轮换

description: 通过对JWKS进行缓存与kid精确匹配、TTL上限控制与失败回退策略,保障公钥轮换期间JWT验签稳定与安全。

categories:

  • 文章资讯
  • 编程技术

---

背景与价值

JWKS公钥轮换要求资源端快速适配。缓存TTL与回退策略可降低失败概率并保障验签稳定。

统一规范

  • 域白名单:只允许受控 jwks_uri 域名。
  • TTL上限:缓存不超过上限(如30分钟)且不超过响应提示。
  • 回退策略:当前kid缺失时尝试刷新,失败则回退至上一版并记录审计。

核心实现

JWKS抓取与缓存

type Jwk = { kid: string; kty: string; crv?: string; n?: string; e?: string }
type Jwks = { keys: Jwk[] }

class Cache<T> { data = new Map<string, { v: T; until: number }>(); get(k: string): T | undefined { const e = this.data.get(k); if (!e) return; if (Date.now() > e.until) { this.data.delete(k); return } return e.v } set(k: string, v: T, ttlMs: number) { this.data.set(k, { v, until: Date.now() + ttlMs }) } }

const allowOrigins = new Set(['https://auth.example.com'])

function originAllowed(url: string): boolean { try { const u = new URL(url); return allowOrigins.has(u.origin) } catch { return false } }

async function fetchJwks(uri: string): Promise<Jwks | null> {
  if (!originAllowed(uri)) return null
  const r = await fetch(uri, { headers: { 'accept': 'application/json' } })
  if (!r.ok) return null
  return r.json()
}

验签与回退

function base64urlToBuf(s: string): ArrayBuffer { const b = atob(s.replace(/-/g,'+').replace(/_/g,'/')); const u = new Uint8Array(b.length); for (let i=0;i<b.length;i++) u[i] = b.charCodeAt(i); return u.buffer }

async function importRsa(nB64: string, eB64: string): Promise<CryptoKey> {
  const jwk: JsonWebKey = { kty: 'RSA', n: nB64, e: eB64, ext: true }
  return crypto.subtle.importKey('jwk', jwk, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify'])
}

type JwtHeader = { alg: 'RS256' | 'ES256'; kid?: string }
type VerifyCtx = { jwksUri: string; cache: Cache<Jwks>; maxTtlMs: number }

async function verifyJwt(jwt: string, ctx: VerifyCtx): Promise<boolean> {
  const parts = jwt.split('.')
  if (parts.length !== 3) return false
  const [h, p, s] = parts
  const header: JwtHeader = JSON.parse(new TextDecoder().decode(base64urlToBuf(h)))
  if (header.alg !== 'RS256') return false
  let jwks = ctx.cache.get(ctx.jwksUri)
  let triedRefresh = false
  async function ensureJwks(): Promise<Jwks | null> {
    if (jwks) return jwks
    const fresh = await fetchJwks(ctx.jwksUri)
    if (!fresh) return null
    ctx.cache.set(ctx.jwksUri, fresh, ctx.maxTtlMs)
    jwks = fresh
    triedRefresh = true
    return fresh
  }
  jwks = jwks || await ensureJwks()
  if (!jwks) return false
  let key = jwks.keys.find(k => k.kid === header.kid && k.kty === 'RSA')
  if (!key && !triedRefresh) { jwks = await ensureJwks(); key = jwks?.keys.find(k => k.kid === header.kid && k.kty === 'RSA') || undefined }
  if (!key || !key.n || !key.e) return false
  const pub = await importRsa(key.n, key.e)
  const sig = base64urlToBuf(s)
  const ok = await crypto.subtle.verify({ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, pub, sig, new TextEncoder().encode(h + '.' + p))
  return !!ok
}

落地建议

  • jwks_uri 实施域白名单;缓存TTL不超过上限与响应提示。
  • 当kid缺失或不匹配时触发刷新与回退审计,失败拒绝访问。

验证清单

  • 缓存是否在TTL内命中;kid是否精确匹配;失败是否触发刷新与回退。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部