Appearance
Cloudflare Worker 给 Decap CMS 做 GitHub OAuth 网关
背景
Decap CMS(之前的 Netlify CMS)改文件需要 GitHub Token,自己写 OAuth 太麻烦,写一个 Worker 当网关就行。
实现原理
text
Decap CMS → /auth → Cloudflare Worker → github.com/login/oauth/authorize
↓
github.com/login/oauth/access_token ← /callback
↓
Worker → Decap CMS(postMessage token)完整代码
javascript
// cloudflare/decap-oauth-worker.js
// Decap CMS GitHub OAuth gateway for Cloudflare Workers
const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize'
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
function html(body) {
return new Response(body, {
headers: {
'content-type': 'text/html; charset=utf-8',
'cache-control': 'no-store'
}
})
}
function popupScript(payload) {
const safePayload = JSON.stringify(payload).replace(/</g, '\\u003c')
return `<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>GitHub 登录完成</title></head>
<body>
<script>
(function() {
var payload = ${safePayload};
function send() {
if (window.opener) {
window.opener.postMessage('authorization:github:success:' + JSON.stringify(payload), '*');
}
window.close();
}
send();
setTimeout(send, 500);
})();
</script>
<p>GitHub 登录完成,可以关闭此窗口。</p>
</body>
</html>`
}
function errorScript(message) {
const safeMessage = JSON.stringify({ error: message }).replace(/</g, '\\u003c')
return `<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>登录失败</title></head>
<body>
<script>
(function() {
var payload = ${safeMessage};
if (window.opener) {
window.opener.postMessage('authorization:github:error:' + JSON.stringify(payload), '*');
}
window.close();
})();
</script>
<p>登录失败:${message}</p>
</body>
</html>`
}
export default {
async fetch(request, env) {
const url = new URL(request.url)
if (!env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET) {
return new Response('Missing credentials', { status: 500 })
}
if (url.pathname === '/auth') {
const scope = url.searchParams.get('scope') || 'repo,user'
const siteId = url.searchParams.get('site_id') || 'maixuzi'
const state = crypto.randomUUID() + ':' + siteId
const redirect = new URL(GITHUB_AUTHORIZE_URL)
redirect.searchParams.set('client_id', env.GITHUB_CLIENT_ID)
redirect.searchParams.set('redirect_uri', `${url.origin}/callback`)
redirect.searchParams.set('scope', scope)
redirect.searchParams.set('state', state)
return Response.redirect(redirect.toString(), 302)
}
if (url.pathname === '/callback') {
const code = url.searchParams.get('code')
if (!code) {
return html(errorScript('Missing code'))
}
const tokenResponse = await fetch(GITHUB_TOKEN_URL, {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'user-agent': 'maixuzi-decap-oauth'
},
body: JSON.stringify({
client_id: env.GITHUB_CLIENT_ID,
client_secret: env.GITHUB_CLIENT_SECRET,
code,
redirect_uri: `${url.origin}/callback`
})
})
const tokenData = await tokenResponse.json()
if (!tokenResponse.ok || tokenData.error || !tokenData.access_token) {
return html(errorScript(tokenData.error_description || tokenData.error || 'Token exchange failed'))
}
return html(popupScript({ token: tokenData.access_token, provider: 'github' }))
}
return new Response('Not found', { status: 404 })
}
}配置
wrangler.toml
toml
name = "maixuzi-cms-oauth"
main = "decap-oauth-worker.js"
compatibility_date = "2026-06-20"
[vars]
GITHUB_CLIENT_ID = "xxx"Secrets
bash
wrangler secret put GITHUB_CLIENT_SECRET
# 输入 GitHub OAuth App 的 client secretGitHub OAuth App
GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
| 配置 | 值 |
|---|---|
| Homepage URL | https://maixuzi.cn |
| Authorization callback URL | https://maixuzi-cms-oauth.xxx.workers.dev/callback |
Decap CMS 配置
yaml
backend:
name: github
repo: lingyun15/maixuzi-site
branch: main
base_url: https://maixuzi.cn
auth_endpoint: auth # Cloudflare Worker 路径部署
bash
cd cloudflare
wrangler deploy效果
可以在 https://maixuzi.cn/admin/ 用 GitHub 登录直接编辑内容,所有变更会 commit 到 main 分支,Cloudflare Pages 自动重新部署。
完整项目
更多内容:https://maixuzi.cn