最近在折腾我们的统一登录系统 CZLConnect 时,为了更好地适配纯前端的单页应用(比如咱们常用的 Vite/Next.js 写的项目)以及桌面/移动端应用,我给系统新增了 OAuth2 的 PKCE (Proof Key for Code Exchange) 授权流程支持。
老实说,这次接入主要是借助 Codex+Claude 帮我一把梭哈生成的代码。跑起来确实丝般顺滑,但作为一个有追求的开发者,知其然还要知其所以然。这两天我把 AI 写的底层逻辑彻底盘了一遍,发现它的设计非常巧妙。今天就在这里给大家做一个通俗易懂的拆解,希望能帮到后续准备接入 CZLConnect 的兄弟们。
为什么我们需要 PKCE?
以前我们在做后端对接时,标准的 OAuth2 流程是:拿 Code + Client Secret 去换取 Token。
但这有个致命问题:如果是纯前端应用或客户端 App,代码是暴露在用户设备上的。如果你把 Client Secret 写在代码里,别人只要稍微抓个包或者逆向一下,你的“绝对机密”就底裤掉光了。
没有 Secret 怎么证明我是我? PKCE 的核心思想就是:既然静态密码藏不住,那我就在每次登录时,现场随机捏造一个“一次性密码”。
核心逻辑拆解(AI 到底写了些什么?)
Codex 帮我写的代码,本质上是在前端/客户端完成了以下四个关键步骤。为了方便大家理解,我把复杂的代码翻译成了咱们都能看懂的逻辑流程:
第一步:生成“一次性密码”(code_verifier)
在用户点击“登录”按钮的那一瞬间,客户端会在本地随机生成一串 43 到 128 位的字符串。这串字符叫 code_verifier。
- 注意: 这个原始字符串只能偷偷藏在浏览器的内存或
sessionStorage里,绝对不能泄露。
第二步:给密码套上“不可逆的马甲”(code_challenge)
因为稍后要把密码发给认证服务器,直接发容易被拦截。所以,客户端使用 SHA-256 算法,把刚才的 code_verifier 绞碎,算出一个哈希值,然后再进行 Base64URL 编码。这个算出来的值叫 code_challenge。
- (科普:SHA-256 是单向的,即便黑客拿到了
code_challenge,数学上也绝对无法反推出原始的code_verifier)。
第三步:带着“马甲”去登录
客户端把用户重定向到 CZLConnect 的登录页,并在 URL 参数里带上刚才算出来的 code_challenge 和加密方式(通常是 S256)。
https://connect.czl.net/authorize?
response_type=code&
client_id=你的客户端ID&
code_challenge=刚才算出的哈希值&
code_challenge_method=S256&
redirect_uri=你的回调地址
- CZLConnect 此时的内心OS: “好的,我记住你了。等你一会拿着 Code 来换 Token 时,必须交出与这个哈希值匹配的原始暗号。”
第四步:亮出“底牌”换取 Token (防拦截的关键)
用户在 CZLConnect 登录成功后,带着授权码 Code 跳回你的应用。
此时,你的应用向服务器发送请求换取 Token,最关键的一步来了:请求里不再传 client_secret,而是传第一步里藏在本地的原始暗号 code_verifier!
POST https://connect.czl.net/token
{
"grant_type": "authorization_code",
"client_id": "你的客户端ID",
"code": "拿到的授权码",
"redirect_uri": "你的回调地址",
"code_verifier": "第一步生成的原始字符串!"
}
服务器拿到 code_verifier 后,自己用 SHA-256 算一遍。如果算出来的结果刚好等于第三步里记录的 code_challenge,验证通过,下发 Token!
哪怕黑客在第四步之前拦截了 Code,因为他没有你本地存的 code_verifier,去服务器换 Token 时就会被直接拒绝。
核心代码参考 (前端 Web Crypto API)
为了方便大家接入,我把这部分最核心的加密逻辑抽出来了,现代浏览器不需要额外引包,直接用原生 API 就能搞定:
// 1. 生成随机的 code_verifier
function generateCodeVerifier() {
const array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// 2. 将 verifier 转换为 SHA-256 并进行 Base64URL 编码得到 code_challenge
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode.apply(null, [...new Uint8Array(digest)]))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
总结
PKCE 看似复杂,其实就是一个**“客户端先给出哈希值,后续再交出原值以验明正身”**的闭环。搞懂了这个逻辑,不管是排查登录报错,还是后续自己手写 SDK,心里都有底了。
目前 CZLConnect 的 PKCE 接口已经稳定上线,欢迎有单机/纯前端应用需求的小伙伴们接入体验。测试过程中遇到任何坑,或者对认证流程有什么疑问,欢迎在帖子下面跟帖交流!