背景与价值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 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部