给 CZLConnect 加上 OAuth2 PKCE 支持,顺便拆解一下它的底层逻辑

最近在折腾我们的统一登录系统 CZLConnect 时,为了更好地适配纯前端的单页应用(比如咱们常用的 Vite/Next.js 写的项目)以及桌面/移动端应用,我给系统新增了 OAuth2 的 PKCE (Proof Key for Code Exchange) 授权流程支持。

老实说,这次接入主要是借助 Codex+Claude 帮我一把梭哈生成的代码。跑起来确实丝般顺滑,但作为一个有追求的开发者,知其然还要知其所以然。这两天我把 AI 写的底层逻辑彻底盘了一遍,发现它的设计非常巧妙。今天就在这里给大家做一个通俗易懂的拆解,希望能帮到后续准备接入 CZLConnect 的兄弟们。


:thinking: 为什么我们需要 PKCE?

以前我们在做后端对接时,标准的 OAuth2 流程是:拿 Code + Client Secret 去换取 Token

但这有个致命问题:如果是纯前端应用或客户端 App,代码是暴露在用户设备上的。如果你把 Client Secret 写在代码里,别人只要稍微抓个包或者逆向一下,你的“绝对机密”就底裤掉光了。

没有 Secret 怎么证明我是我? PKCE 的核心思想就是:既然静态密码藏不住,那我就在每次登录时,现场随机捏造一个“一次性密码”


:hammer_and_wrench: 核心逻辑拆解(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 时就会被直接拒绝。


:laptop: 核心代码参考 (前端 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 接口已经稳定上线,欢迎有单机/纯前端应用需求的小伙伴们接入体验。测试过程中遇到任何坑,或者对认证流程有什么疑问,欢迎在帖子下面跟帖交流!