---
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校验
```ts
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 {
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 {
if (method !== 'S256') return false
if (!validVerifier(verifier)) return false
const calc = await s256(verifier)
return calc === challenge
}
```
重定向URI白名单
```ts
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关联与回调校验
```ts
type Session = { state: string; nonce: string }
const sessionById = new Map()
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 {
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` 是否与会话记录一致。
发表评论 取消回复