OAuth2/OIDC PKCE 前端安全实践:授权码流程、令牌管理与合规技术背景PKCE 为公开客户端(SPA)提供安全的授权码流程,避免拦截与重放风险。前端通过生成 `code_verifier/code_challenge`,完成授权与回调交换,并采用内存存储与短期刷新策略保障令牌安全。核心内容生成 code_verifier/code_challengefunction base64UrlEncode(buf: ArrayBuffer) {
const bytes = new Uint8Array(buf);
let str = '';
for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
async function createPKCE() {
const verifierBytes = crypto.getRandomValues(new Uint8Array(32));
const code_verifier = base64UrlEncode(verifierBytes.buffer);
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(code_verifier));
const code_challenge = base64UrlEncode(digest);
return { code_verifier, code_challenge };
}
发起授权与回调处理const OIDC = {
authEndpoint: 'https://auth.example.com/authorize',
tokenEndpoint: 'https://auth.example.com/token',
clientId: 'spa-client',
redirectUri: 'https://app.example.com/callback',
scope: 'openid profile email'
};
async function beginLogin() {
const { code_verifier, code_challenge } = await createPKCE();
sessionStorage.setItem('pkce_verifier', code_verifier);
const url = new URL(OIDC.authEndpoint);
url.searchParams.set('response_type', 'code');
url.searchParams.set('client_id', OIDC.clientId);
url.searchParams.set('redirect_uri', OIDC.redirectUri);
url.searchParams.set('scope', OIDC.scope);
url.searchParams.set('code_challenge', code_challenge);
url.searchParams.set('code_challenge_method', 'S256');
location.href = url.toString();
}
async function handleCallback() {
const params = new URLSearchParams(location.search);
const code = params.get('code');
const verifier = sessionStorage.getItem('pkce_verifier') || '';
if (!code || !verifier) throw new Error('missing code or verifier');
const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: OIDC.clientId,
code,
redirect_uri: OIDC.redirectUri,
code_verifier: verifier
});
const res = await fetch(OIDC.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
const tokens = await res.json();
TokenStore.set(tokens);
history.replaceState(null, '', '/');
}
令牌安全管理与刷新type Tokens = { access_token: string; id_token?: string; refresh_token?: string; expires_in: number; token_type: string };
const TokenStore = (() => {
let tokens: Tokens | null = null;
let expiry = 0;
return {
set: (t: Tokens) => { tokens = t; expiry = Date.now() + (t.expires_in - 30) * 1000; },
get: () => tokens,
clear: () => { tokens = null; expiry = 0; },
valid: () => tokens && Date.now() < expiry
};
})();
async function authFetch(input: RequestInfo, init: RequestInit = {}) {
if (!TokenStore.valid()) await refreshTokens();
const t = TokenStore.get();
const headers = new Headers(init.headers || {});
if (t) headers.set('Authorization', `${t.token_type} ${t.access_token}`);
return fetch(input, { ...init, headers, credentials: 'include' });
}
async function refreshTokens() {
const t = TokenStore.get();
if (!t?.refresh_token) { TokenStore.clear(); return; }
const body = new URLSearchParams({ grant_type: 'refresh_token', client_id: OIDC.clientId, refresh_token: t.refresh_token });
const res = await fetch(OIDC.tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body });
const nt = await res.json();
TokenStore.set(nt);
}
合规与隐私- 令牌存储优先内存;必要时短期存储在 sessionStorage,避免 localStorage
- 使用 HTTPS 与严格 CSP,禁止第三方脚本访问令牌
- 采用最小权限 scope 与短有效期,降低风险
技术验证参数在 Chrome 128/Edge 130(Windows/macOS)下:登录成功率:≥ 98%刷新成功率:≥ 95%令牌泄露风险(静态扫描):0回调交换耗时:P95 350–900ms应用场景企业与消费级应用的统一登录多端统一身份与权限控制合规要求严格的安全场景最佳实践优先 PKCE 授权码流程,不使用隐式流令牌仅在请求前注入,避免全局暴露失败时清理状态并回到登录入口,保障安全

发表评论 取消回复