---

title: OIDC ID Token验证与时钟偏移(aud/iss/nonce/exp)最佳实践

keywords:

  • OIDC
  • ID Token
  • iss
  • aud
  • nonce
  • exp
  • leeway
  • JWS验证

description: 通过JWS验签与iss/aud/nonce/exp校验并引入时钟偏移容忍窗口,保障OIDC ID Token在不同环境下的稳健可信。

categories:

  • 文章资讯
  • 编程技术

---

背景与价值

ID Token是登录流程核心。结合验签、声明校验与时钟偏移容忍,能防止伪造与重放问题并提升跨环境稳定性。

统一规范

  • 验签算法:统一 RS256/ES256,拒绝不安全算法。
  • 发行者与受众:iss/aud 精确匹配;azp 可选辅助校验。
  • 时间窗口:为 iat/exp 提供 ±300 秒容忍,避免小幅时钟漂移导致误判。
  • nonce绑定:nonce 与会话一致且一次性使用。

核心实现

JWS验签与声明校验

type Claims = { iss: string; aud: string | string[]; exp: number; iat: number; nonce?: string; azp?: string }

function enc(s: string): Uint8Array { return new TextEncoder().encode(s) }

async function importKey(spki: ArrayBuffer, type: 'RS256'|'ES256'): Promise<CryptoKey> {
  const algo = type === 'RS256' ? { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' } : { name: 'ECDSA', namedCurve: 'P-256' }
  return crypto.subtle.importKey('spki', spki, algo as any, false, ['verify'])
}

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 verifyJws(jws: string, key: CryptoKey, type: 'RS256'|'ES256'): Promise<Claims | null> {
  const parts = jws.split('.')
  if (parts.length !== 3) return null
  const [h, p, s] = parts
  const header = JSON.parse(new TextDecoder().decode(base64urlToBuf(h)))
  if (header.alg !== type) return null
  const sig = base64urlToBuf(s)
  const ok = await crypto.subtle.verify(type === 'RS256' ? { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' } : { name: 'ECDSA', hash: 'SHA-256' }, key, sig, enc(h + '.' + p))
  if (!ok) return null
  const claims: Claims = JSON.parse(new TextDecoder().decode(base64urlToBuf(p)))
  return claims
}

function timeNow(): number { return Math.floor(Date.now() / 1000) }

function validAud(aud: string | string[], expected: string): boolean { return Array.isArray(aud) ? aud.includes(expected) : aud === expected }

function claimsOk(c: Claims, expected: { iss: string; aud: string; nonce?: string; leewaySec: number }): boolean {
  if (c.iss !== expected.iss) return false
  if (!validAud(c.aud, expected.aud)) return false
  const now = timeNow()
  if (c.exp + expected.leewaySec < now) return false
  if (c.iat - expected.leewaySec > now) return false
  if (expected.nonce && c.nonce !== expected.nonce) return false
  return true
}

落地建议

  • 验签算法限定为 RS256/ES256,拒绝 none 与弱算法;确保公钥来源可信并定期轮换。
  • iss/aud 精确匹配,nonce 与会话绑定且一次性使用,过期后拒绝。
  • 针对 iat/exp 引入合适的偏移窗口(建议300秒),缓解小幅时钟漂移。

验证清单

  • JWS验签是否成功且算法符合要求。
  • iss/aud/nonce 是否与期望一致。
  • iat/exp 是否在容忍窗口内。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部