用 GitHub OAuth + Cloudflare Worker KV 给静态博客加鉴权
起因是想在博客上加一个写作入口——右下角一个悬浮按钮,登录后可以直接在浏览器里写文章、上传图片,提交到 GitHub 仓库,CI 自动构建发布。听起来不复杂,但静态博客没有后端,不能直接处理 OAuth 换 token,也不能暴露 GitHub 的 Client Secret。于是引入了 Cloudflare Worker 作为中间层。
GitHub OAuth 是怎么工作的
GitHub OAuth 走的是标准的 Authorization Code Flow,分三步:
- 前端重定向到 GitHub 授权页,带上
client_id、scope、state(防 CSRF 的随机串)、redirect_uri - 用户授权后,GitHub 回调
redirect_uri,在 URL 里带上code和state - 用
code换access_token,这一步需要client_secret,必须在后端做
第三步就是问题所在。Client Secret 不能出现在前端代码里,所以需要一个后端来做这个换 token 的动作。我用 Cloudflare Worker 的 /exchange 接口接收 code,在 Worker 里用 secret 换 token,然后把 token 存到 KV,只把 session_id 返回给前端。
前端整个流程是:
- 点登录 → 生成随机
state存 sessionStorage → 跳转 GitHub - GitHub 回调 → 验证
state一致 → 把codePOST 给 Worker - Worker 换 token,存 KV,返回
session_id - 前端把
session_id存 localStorage,后续每次操作带上它
踩坑:OAuth 回调 URL 的 trailing slash
我的博客用 Caddy 做反向代理,Caddy 对没有 trailing slash 的路径会 301 重定向并加上 /,但重定向时会丢掉 query string,导致 ?code=xxx&state=xxx 全部丢失。
调试方法:在回调页把 code 和 state 都打印出来,结果显示 code=null | state=null,说明 URL 参数根本没到页面。
解法很简单:redirect_uri 末尾加 /,让 Caddy 不触发重定向。
Cloudflare Worker KV 是什么
KV 是 Cloudflare 提供的全局分布式键值存储,可以在 Worker 里直接读写,非常适合存 session。
创建 KV namespace:
wrangler kv namespace create SESSIONS会返回一个 namespace ID,填到 wrangler.toml 里:
[[kv_namespaces]]binding = "SESSIONS"id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"在 Worker 里用:
// 写入,TTL 7 天await env.SESSIONS.put(session_id, token, { expirationTtl: 7 * 24 * 3600 })
// 读取const token = await env.SESSIONS.get(session_id)
// 删除await env.SESSIONS.delete(session_id)env.SESSIONS 就是绑定的 KV namespace,直接用,不需要额外 import。expirationTtl 单位是秒,到期自动清理,不用自己管。
敏感配置怎么放:
client_id 这种公开的放 wrangler.toml 的 [vars] 里没问题,但 client_secret 不能进代码仓库,用 wrangler secret:
wrangler secret put GITHUB_CLIENT_SECRET命令行会提示输入值,之后在 Worker 里通过 env.GITHUB_CLIENT_SECRET 读取,不会出现在任何文件里。
GitHub Contents API 提交文件
用 Worker 代持 token 之后,提交文件走的是 GitHub REST API:
PUT /repos/{owner}/{repo}/contents/{path}body 是:
{ "message": "feat: add post", "content": "<base64 编码的文件内容>"}图片等二进制文件也一样,直接传 base64 就行。
踩坑:中文路径
文章目录带中文时,直接拼接 URL 会 400。GitHub API 要求路径里每一段都要 URL 编码:
const encodedPath = path.split('/').map(encodeURIComponent).join('/')不能直接 encodeURIComponent(path),那会把 / 也编码掉。
整体架构回顾
浏览器(静态页面) ↓ 1. 重定向到 GitHub OAuthGitHub ↓ 2. 回调带 code浏览器 ↓ 3. POST code 给 WorkerCloudflare Worker ↓ 4. 用 client_secret 换 token,存 KV ↓ 5. 返回 session_id浏览器(localStorage 存 session_id) ↓ 6. 写文章,带 session_id 提交给 WorkerCloudflare Worker ↓ 7. 从 KV 取 token,调 GitHub API 提交文件GitHub 仓库 → CI 构建 → 博客更新token 全程在 Worker 和 KV 里流转,浏览器只持有 session_id,即使 localStorage 被读也拿不到真正的 GitHub token。
待探索的问题
- KV 是最终一致的,理论上极端情况下刚写入的 session 在另一个 edge 节点可能读不到——实际使用中暂时没遇到,但值得注意
- Worker 现在没有限速,如果 session_id 泄露,理论上可以无限调用 GitHub API;可以加请求频率限制
- 图片上传是逐张串行的,文章图片多时会慢,可以改并行