---
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是否与会话记录一致。

发表评论 取消回复