Decap CMS 登录循环排查
现象

博客后台使用 Decap CMS,GitHub 登录走自建 OAuth 代理。流程看起来正常:点击 /admin/ 里的 “Login with GitHub”,跳转 GitHub,授权成功,然后回到站点。
但结果是:CMS 仍停在登录页,像是 token 从来没有传回来。
这类问题最容易误判成 GitHub OAuth 配置错误。实际排查下来,根因在 Decap CMS 的弹窗通信协议和 origin 校验。
当前登录链路
浏览器 /admin/
↓
Decap CMS 打开 OAuth 弹窗
↓
/oauth/auth → GitHub 授权
↓
/oauth/callback → OAuth 代理换 token
↓
弹窗把 token 传回 CMS 主窗口
关键点是最后一步:Decap CMS 不是单纯依赖 URL 重定向,而是用弹窗和主窗口之间的 postMessage 完成认证。
第一处错误:回调路径不对
最初的 OAuth 代理在拿到 token 后执行:
res.redirect(sd.redirect_uri + '/#/callback?access_token=' + token);
这个路径会跳到站点根目录下的 /#/callback,但 CMS 后台在 /admin/。所以第一版修复是改成:
/admin/#/callback
这一步解决了路径问题,但登录仍然失败。说明问题不只是“跳错页面”。
第二处错误:把弹窗协议当成重定向协议
继续看 Decap CMS 的认证流程后,发现它使用的是弹窗握手:
CMS 主窗口 OAuth 弹窗
│ │
│ window.open(auth_url) ───────────────> │
│ │
│ <──── authorizing:github │
│ │
│ ───── 确认消息 ─────────────────────> │
│ │
│ <──── authorization:github:success:... │
│ │
│ 关闭弹窗,完成登录 │
也就是说,/oauth/callback 不应该只做 redirect,而应该返回一个 HTML 页面,由这个页面在弹窗里执行脚本,把 token 用 postMessage 发给 opener。
第三处错误:origin 校验失败
我把 callback 改成 postMessage 后,弹窗能发出第一条消息:
authorizing:github
调试日志也显示 CMS 主窗口收到了:
[DEBUG] CMS got message: "authorizing:github" origin: https://techloop.online
但 CMS 没有回复握手确认。问题出在 Decap CMS 内部的校验逻辑:
handshakeCallback(e, t) {
const r = (n) => {
if (n.data === "authorizing:" + e.provider && n.origin === this.base_url) {
window.opener.postMessage(n.data, n.origin);
}
};
return r;
}
它要求 n.origin === this.base_url。而当时的配置是:
backend:
base_url: https://techloop.online/oauth
浏览器事件里的 origin 只有协议、域名和端口,不包含路径。因此:
n.origin = https://techloop.online
base_url = https://techloop.online/oauth
两者不相等,CMS 直接忽略了这条消息。
最终修复
先改 Decap CMS 配置,把域名和路径拆开:
backend:
name: github
repo: your-username/your-repo
branch: main
base_url: https://techloop.online
auth_endpoint: oauth/auth
再让 OAuth 代理的 /callback 返回 HTML,并按 Decap CMS 期望的消息格式发送 token:
app.get('/callback', async (req, res) => {
// ... 用 code 换取 access_token ...
const content = JSON.stringify({ token: td.access_token, provider: 'github' });
res.set('Content-Type', 'text/html');
res.send('<!DOCTYPE html><html><body><script>' +
'(function(){' +
'var d=' + content + ';' +
'var msg="authorization:github:success:"+JSON.stringify(d);' +
'function receiveMessage(e){' +
'window.opener.postMessage(msg,e.origin);' +
'window.removeEventListener("message",receiveMessage,false);' +
'}' +
'window.addEventListener("message",receiveMessage,false);' +
'window.opener.postMessage("authorizing:github","*");' +
'setTimeout(function(){' +
'window.opener.postMessage(msg,"https://your-domain.com");' +
'},1500);' +
'})()' +
'</script></body></html>');
});
这里保留了一个 1.5 秒超时回退。正常情况下,弹窗会先发送 authorizing:github,等主窗口回复后再把 token 发回去;如果握手因为时序问题失败,回退逻辑至少还能再尝试一次。
根因整理
| 层级 | 问题 | 修复 |
|---|---|---|
| 回调路径 | 跳到 /#/callback |
改为 CMS 所在路径或使用弹窗页面 |
| 通信方式 | 用重定向传 token | 改为 postMessage |
| origin 校验 | base_url 带 /oauth |
base_url 只保留 origin |
真正卡住登录的是第三项。前两项会让流程走偏,第三项则会让 CMS 明明收到了消息,却因为校验失败不处理。
这次排查的经验
先确认框架的认证模型
Decap CMS 的 GitHub 登录不是普通页面跳转,而是弹窗通信。协议理解错了,后面修路径、改 redirect 都只能碰运气。区分 origin 和 URL
postMessage里的origin是protocol://host:port,不包含 path。凡是用 origin 做严格比较的地方,都不能把路径混进去。源码比猜测更快
最关键的信息来自 Decap CMS 打包代码里的handshakeCallback。看到n.origin === this.base_url后,问题基本就确定了。给弹窗握手留回退
弹窗通信依赖两个窗口的加载时序,生产环境里加一个短延迟回退能减少偶发失败。
Decap CMS 期望的消息格式
| 步骤 | 方向 | 消息内容 | targetOrigin |
|---|---|---|---|
| 1 | 弹窗 → 主窗口 | "authorizing:github" |
"*" |
| 2 | 主窗口 → 弹窗 | 握手确认 | e.origin |
| 3 | 弹窗 → 主窗口 | "authorization:github:success:{...}" |
e.origin |
成功消息里的 JSON 至少需要包含:
{
"token": "...",
"provider": "github"
}
修复后,GitHub 授权、弹窗回传 token、CMS 写入登录状态这三步才完整闭环。
See also
- 一次完整的 WSL2 安装排障:从 BIOS、TPM 判断到 Windows 11 修复安装 2026-07-07
- 把一个 SPA 博客补成可预渲染、可同步、可持续部署 2026-05-29
- 把宿舍 Windows 主机改成可远程训练的 WSL 工作站 2026-05-27
- 把每日科技日报改成服务器自运行 2026-05-26
- Chrome 一打开就跳到 360 导航页?按这份手册一步步修复 2026-05-20