---

title: OAuth PKCE与授权码拦截防护最佳实践

keywords:

  • PKCE
  • S256
  • code_verifier
  • code_challenge
  • redirect_uri白名单
  • state
  • nonce
  • 拦截防护

description: 通过严格的PKCE S256校验、state/nonce对齐和redirect_uri白名单,降低授权码拦截与重放风险,保障移动与SPA流程安全。

categories:

  • 文章资讯
  • 技术教程

---

背景与价值

授权码拦截与重放可导致令牌被盗。PKCE(S256)与 state/nonce 联动以及 redirect_uri 白名单能有效降低风险,适用于移动端与SPA。

统一规范

  • 方法限定:仅允许 S256,禁止 plain
  • 验证器长度与字符集:code_verifier 长度 43-128,字符集 [A-Za-z0-9-._~]
  • 重定向白名单:https 域名与路径严格匹配,必要时仅允许 http://localhost 开发例外。
  • 关联校验:state 与会话绑定,nonce 与ID Token匹配。

核心实现

PKCE校验

function b64url(bytes: ArrayBuffer): string {
  const u = new Uint8Array(bytes)
  let s = ''
  for (let i = 0; i < u.length; i++) s += String.fromCharCode(u[i])
  return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/,'')
}

function validVerifier(v: string): boolean {
  return /^[A-Za-z0-9\-\._~]{43,128}$/.test(v)
}

async function s256(v: string): Promise<string> {
  const d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(v))
  return b64url(d)
}

async function pkceOk(verifier: string, challenge: string, method: string): Promise<boolean> {
  if (method !== 'S256') return false
  if (!validVerifier(verifier)) return false
  const calc = await s256(verifier)
  return calc === challenge
}

重定向URI白名单

const allowRedirects = new Set([
  'https://app.example.com/callback',
  'https://mobile.example.com/auth/callback',
  'http://localhost:3000/callback'
])

function redirectAllowed(uri: string): boolean {
  try {
    const u = new URL(uri)
    if (u.protocol === 'http:' && u.hostname !== 'localhost') return false
    return allowRedirects.has(u.origin + u.pathname)
  } catch {
    return false
  }
}

state/nonce关联与回调校验

type Session = { state: string; nonce: string }
const sessionById = new Map<string, Session>()

function putSession(id: string, s: Session) { sessionById.set(id, s) }
function getSession(id: string): Session | undefined { return sessionById.get(id) }

type Callback = { code: string; state: string; redirect_uri: string; code_verifier: string; code_challenge: string; code_challenge_method: string; id_token_nonce?: string }

async function verifyCallback(sessId: string, cb: Callback): Promise<boolean> {
  const sess = getSession(sessId)
  if (!sess) return false
  if (cb.state !== sess.state) return false
  if (cb.id_token_nonce && cb.id_token_nonce !== sess.nonce) return false
  if (!redirectAllowed(cb.redirect_uri)) return false
  return pkceOk(cb.code_verifier, cb.code_challenge, cb.code_challenge_method)
}

落地建议

  • 强制 S256,拒绝 plain,并校验 code_verifier 长度与字符集。
  • redirect_uri 实施精确白名单匹配,开发例外仅限本地 localhost
  • state/nonce 与会话绑定,回调时逐项对齐校验并记录审计。
  • 结合授权服务器的 code 一次性使用与短期有效策略,减少拦截面。

验证清单

  • code_verifier 是否满足长度与字符集约束。
  • code_challenge_method 是否为 S256 且校验通过。
  • redirect_uri 是否命中白名单且协议安全。
  • state/nonce 是否与会话记录一致。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部