Last updated on

OAuth 学习笔记 - Decap CMS 认证原理与实现


一、OAuth 是什么?

1.1 定义

OAuth (Open Authorization) 是一种开放标准授权协议,允许用户让第三方应用访问他们在其他服务上的资源,而不需要将用户名和密码提供给第三方应用。

1.2 核心概念

  • 资源拥有者 (Resource Owner):能够授权访问受保护资源的实体(通常是用户)
  • 客户端 (Client):代表资源拥有者请求访问受保护资源的第三方应用(Decap CMS)
  • 授权服务器 (Authorization Server):验证用户身份并颁发访问令牌的服务器(GitHub)
  • 资源服务器 (Resource Server):托管受保护资源的服务器(GitHub API)

1.3 关键术语

  • Client ID:客户端标识符,公开可见
  • Client Secret:客户端密钥,必须保密,不能暴露在客户端代码中
  • Access Token:访问令牌,用于访问受保护资源
  • Authorization Code:授权码,临时授权凭证,用于换取 Access Token

二、OAuth 认证流程详解

2.1 标准 OAuth 2.0 授权码模式流程

┌─────────────┐                ┌──────────────────┐                ┌─────────────┐
│   Decap CMS │                │  GitHub OAuth    │                │    GitHub   │
│   (Client)  │                │   Authorization  │                │    API      │
└──────┬──────┘                └────────┬─────────┘                └──────┬──────┘
       │                                 │                                 │
       │  1. 访问 /admin                 │                                 │
       │  → 检测未登录                    │                                 │
       │                                 │                                 │
       │  2. 重定向到 GitHub OAuth       │                                 │
       │  → https://github.com/login/    │                                 │
       │     oauth/authorize?client_id=  │                                 │
       │     XXX&redirect_uri=XXX&       │                                 │
       │     scope=repo&state=XXX        │                                 │
       │────────────────────────────────>│                                 │
       │                                 │  3. 显示登录页面                 │
       │                                 │────────────────────────────────>│
       │                                 │  4. 用户授权                     │
       │                                 │<────────────────────────────────│
       │  5. 重定向回回调 URL             │                                 │
       │  ← https://your-site.com/admin/ │                                 │
       │     ?code=AUTH_CODE&state=XXX   │                                 │
       │<────────────────────────────────│                                 │
       │                                 │                                 │
       │  6. 用授权码换取 Access Token    │                                 │
       │  → POST https://github.com/     │                                 │
       │     login/oauth/access_token    │                                 │
       │     (包含 client_secret)        │                                 │
       │────────────────────────────────>│                                 │
       │                                 │  7. 验证授权码,颁发 Token       │
       │  8. 返回 Access Token           │                                 │
       │  ← {"access_token": "ghp_...",  │                                 │
       │       "token_type": "bearer",   │                                 │
       │       "scope": "repo"}          │                                 │
       │<────────────────────────────────│                                 │
       │                                 │                                 │
       │  9. 使用 Token 访问 GitHub API   │                                 │
       │  → Authorization: bearer ghp_...│                                 │
       │────────────────────────────────>│────────────────────────────────>│
       │  10. 返回仓库数据                │                                 │
       │  ← [repo list, content, etc]    │<────────────────────────────────│
       │<────────────────────────────────│                                 │

2.2 流程步骤说明

  1. 用户访问 /admin:Decap CMS 检测用户未登录
  2. 重定向到 GitHub OAuth:携带 client_idredirect_uriscopestate 等参数
  3. 显示登录页面:GitHub 要求用户登录并授权
  4. 用户授权:用户同意授予访问权限
  5. 重定向回回调 URL:GitHub 携带 code(授权码)和 state 回到你的站点
  6. 用授权码换取 Access Token:客户端发送 client_idclient_secretcode 到 GitHub
  7. 验证并颁发 Token:GitHub 验证授权码有效,返回 access_token
  8. 返回 Access Token:客户端获得访问令牌
  9. 访问 GitHub API:使用 access_token 访问受保护资源
  10. 返回数据:GitHub API 返回仓库内容

三、Decap CMS 的 OAuth 实现

3.1 项目架构

你的项目(EdwainPhilo.github.io)
├── src/pages/admin.astro          # CMS 入口页面
├── public/admin/config.yml        # CMS 配置文件
└── src/content/blog/              # 博客内容存储

托管平台:GitHub Pages
OAuth 服务:GitHub OAuth App

3.2 关键配置文件

src/pages/admin.astro

---
---
<link href="/admin/config.yml" type="text/yaml" rel="cms-config-url">
<script src="https://unpkg.com/decap-cms@^3.1.0/dist/decap-cms.js"></script>
<div id="nc-root"></div>

public/admin/config.yml

backend:
  name: github
  repo: EdwainPhilo/EdwainPhilo.github.io
  branch: main
  # 基础 URL:OAuth 代理服务器地址
  base_url: https://your-oauth-proxy.vercel.app
  
media_folder: "src/assets"
public_folder: "/assets"

collections:
  - name: "blog"
    label: "博客"
    folder: "src/content/blog"
    create: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - {label: "标题", name: "title", widget: "string"}
      - {label: "发布日期", name: "date", widget: "datetime"}
      - {label: "内容", name: "body", widget: "markdown"}

3.3 Decap CMS 的默认行为

  • 默认使用 Netlify Identity 服务:https://api.netlify.com/auth
  • 但你的站点托管在 GitHub Pages,Netlify 不认识这个域名
  • 导致 OAuth 认证流程中断,返回 404 错误

四、本地环境 vs 线上环境的区别

4.1 本地环境(localhost)为什么能工作?

原因分析

本地环境流程:

1. 访问 http://localhost:4321/admin
2. Decap CMS 检测到 localhost
3. 可以直接处理 GitHub OAuth 回调
4. 使用 postMessage 通信机制
5. 无需代理服务器也能完成认证

关键点

  • 浏览器安全策略localhost 被视为特殊域名
  • 开发模式:许多工具为本地开发做了特殊处理
  • 直接通信:Decap CMS 可以直接处理 OAuth 回调
  • postMessage 机制:使用 window.postMessage 进行跨窗口通信
// Decap CMS 内部的 OAuth 处理(简化)
window.addEventListener('message', (event) => {
  if (event.data.type === 'OAUTH_SUCCESS') {
    const { access_token } = event.data;
    // 保存 token,完成登录
  }
});

4.2 线上环境(GitHub Pages)为什么 404?

原因分析

线上环境流程:

1. 访问 https://yourname.github.io/admin
2. Decap CMS 检测未登录
3. 尝试重定向到 OAuth 认证
4. 问题出现:
   a. Netlify Identity 不认识 yourname.github.io 域名
   b. 浏览器阻止跨域请求(CORS)
   c. Client Secret 不能暴露在浏览器中
5. 返回 404 错误

核心问题

  1. 跨域问题(CORS)

    浏览器同源策略:
    - yourname.github.io 发起的请求
    - 目标:api.netlify.com
    - 不同源 → 浏览器拦截
  2. Client Secret 安全问题

    严重安全隐患:
    ❌ 将 Client Secret 放在前端代码中
    ❌ 用户可以看到:var CLIENT_SECRET = "your-secret-key";
    ❌ 任何人都可以用这个密钥伪造认证
  3. 域名认证问题

    GitHub OAuth App 配置:
    - 回调 URL 必须预先配置
    - GitHub Pages 的 OAuth 回调需要特殊处理
    - 默认的 Netlify Identity 无法识别 GitHub Pages 域名

五、为什么需要代理服务器?

5.1 核心原因

1. 保护 Client Secret

传统方式(不安全):
浏览器 → 直接请求 GitHub API
         携带 client_secret(暴露在代码中)❌

使用代理服务器(安全):
浏览器 → OAuth 代理服务器 → GitHub API
         不携带 client_secret  ✓
         代理服务器保管 secret  ✓

2. 解决 CORS 问题

直接请求(被拦截):
yourname.github.io → api.netlify.com
(跨域,浏览器拦截)❌

通过代理(解决):
yourname.github.io → oauth-proxy.vercel.app(同域)✓
                      → api.netlify.com(服务器间请求,不受浏览器限制)✓

3. 完成 OAuth 流程

OAuth 流程需要服务器端操作:
1. 接收授权码(code)
2. 验证 state(防止 CSRF)
3. 用 code 换取 access_token
4. 存储 token
5. 返回给客户端

这些操作必须在服务器端完成,不能在浏览器中执行

5.2 形象比喻

比喻一:酒店寄存行李

传统方式(不安全):
你(浏览器)直接把行李(Client Secret)给快递员(GitHub API)
→ 快递员可能不认识你,行李可能丢失 ❌

使用代理(安全):
你 → 酒店前台(代理服务器)→ 快递员
→ 前台验证你的身份,帮你寄存行李 ✓
→ 快递员只和酒店打交道,不直接接触你 ✓

比喻二:装修工人入场

传统方式(不安全):
装修工人(Decap CMS)直接找物业(GitHub)要钥匙
→ 物业不认识工人,不给钥匙 ❌

使用代理(安全):
工人 → 包工头(代理服务器)→ 物业
→ 包工头有合法的授权,向物业领钥匙 ✓
→ 包工头监督工人使用钥匙 ✓

六、解决方案详解

6.1 方案一:Vercel OAuth 服务器(推荐)

优点

  • ✅ 完全免费
  • ✅ 稳定可靠
  • ✅ 配置简单
  • ✅ 自动 HTTPS
  • ✅ 全球 CDN

步骤 1:部署 OAuth 代理服务器

// oauth-proxy.js
module.exports = async (req, res) => {
  const { code, state } = req.query;

  // 用授权码换取 access_token
  const response = await fetch('https://github.com/login/oauth/access_token', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      client_id: process.env.GITHUB_CLIENT_ID,
      client_secret: process.env.GITHUB_CLIENT_SECRET,
      code: code,
    }),
  });

  const data = await response.json();

  // 返回包含 access_token 的 HTML
  res.setHeader('Content-Type', 'text/html');
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <script>
          // 将 token 发送给父窗口
          window.opener.postMessage({
            type: 'OAUTH_SUCCESS',
            access_token: '${data.access_token}',
            token_type: '${data.token_type}'
          }, '*');
          window.close();
        </script>
      </head>
      <body>OAuth 认证成功,正在关闭...</body>
    </html>
  `);
};
// vercel.json
{
  "functions": {
    "api/oauth.js": {
      "memory": 1024
    }
  }
}
// package.json
{
  "name": "decap-cms-oauth-proxy",
  "version": "1.0.0",
  "dependencies": {}
}

步骤 2:配置环境变量

在 Vercel 项目设置中添加:

  • GITHUB_CLIENT_ID: 你的 GitHub OAuth App 的 Client ID
  • GITHUB_CLIENT_SECRET: 你的 GitHub OAuth App 的 Client Secret

步骤 3:部署到 Vercel

# 安装 Vercel CLI
npm i -g vercel

# 登录
vercel login

# 部署
vercel

# 配置环境变量(在 Vercel 控制台添加)

步骤 4:更新 CMS 配置

# public/admin/config.yml
backend:
  name: github
  repo: EdwainPhilo/EdwainPhilo.github.io
  branch: main
  base_url: https://your-oauth-proxy.vercel.app
  # 完整的授权 URL
  auth_endpoint: https://github.com/login/oauth/authorize
  # 代理服务器的回调端点
  auth_url: https://your-oauth-proxy.vercel.app/api/auth
  
media_folder: "src/assets"
public_folder: "/assets"

collections:
  # ... 你的集合配置

6.2 方案二:Cloudflare Worker OAuth 代理

优点

  • ✅ 极其轻量
  • ✅ 快速响应
  • ✅ 完全免费
  • ✅ 边缘计算

实现代码

// oauth-proxy.worker.js
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);

  // OAuth 回调处理
  if (url.pathname === '/auth/callback') {
    const code = url.searchParams.get('code');
    const state = url.searchParams.get('state');

    // 用授权码换取 access_token
    const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'User-Agent': 'Decap-CMS-OAuth-Proxy',
      },
      body: JSON.stringify({
        client_id: GITHUB_CLIENT_ID,
        client_secret: GITHUB_CLIENT_SECRET,
        code: code,
      }),
    });

    const tokenData = await tokenResponse.json();

    // 返回包含 token 的 HTML
    return new Response(`
      <!DOCTYPE html>
      <html>
        <head>
          <script>
            window.opener.postMessage({
              type: 'OAUTH_SUCCESS',
              access_token: '${tokenData.access_token}',
              token_type: '${tokenData.token_type}'
            }, '*');
            window.close();
          </script>
        </head>
        <body>OAuth 认证成功</body>
      </html>
    `, {
      headers: { 'Content-Type': 'text/html' },
    });
  }

  return new Response('Not Found', { status: 404 });
}

部署步骤

# 安装 Wrangler CLI
npm install -g wrangler

# 登录
wrangler login

# 创建 Worker
wrangler init oauth-proxy

# 配置环境变量
wrangler secret put GITHUB_CLIENT_ID
wrangler secret put GITHUB_CLIENT_SECRET

# 部署
wrangler deploy oauth-proxy.worker.js

6.3 方案三:本地开发模式(临时测试)

配置方式

# public/admin/config.yml
backend:
  name: github
  repo: EdwainPhilo/EdwainPhilo.github.io
  branch: main
  # 本地开发模式
  local_backend: true
  
media_folder: "src/assets"
public_folder: "/assets"

collections:
  # ... 你的集合配置

注意事项

  • ⚠️ 仅适用于本地开发
  • ⚠️ 不适用于线上环境
  • ⚠️ 需要启动本地开发服务器
  • ⚠️ 需要安装 netlify-cli
# 安装 netlify-cli
npm install -g netlify-cli

# 启动开发模式
netlify-cms-proxy-server

七、常见问题与注意事项

7.1 GitHub OAuth App 配置

必须配置的参数

Application name: Decap CMS OAuth
Homepage URL: https://yourname.github.io
Application description: OAuth for Decap CMS
Authorization callback URL: https://your-oauth-proxy.vercel.app/api/auth/callback

权限范围(Scope)

repo - 访问私有仓库
public_repo - 访问公开仓库(推荐用于 GitHub Pages)

7.2 常见错误与解决

错误 1:redirect_uri_mismatch

错误信息:
{"error": "redirect_uri_mismatch", "error_description": "..."}

原因:
回调 URL 与 GitHub OAuth App 配置不一致

解决:
1. 检查 GitHub OAuth App 的 Authorization callback URL
2. 确保与代理服务器的 URL 完全一致
3. 注意协议(http vs https)

错误 2:404 Not Found

错误信息:
访问 /admin 时返回 404

原因:
1. base_url 配置错误
2. 代理服务器未部署或访问失败
3. GitHub OAuth App 配置错误

解决:
1. 检查 base_url 是否正确
2. 确认代理服务器可以访问
3. 检查 GitHub OAuth App 配置

错误 3:CORS 错误

错误信息:
Access to fetch at '...' has been blocked by CORS policy

原因:
跨域请求被浏览器拦截

解决:
1. 使用代理服务器
2. 配置 CORS 头部
3. 确保代理服务器返回正确的 CORS 头

7.3 安全最佳实践

✅ 应该做的

  • 在服务器端存储 Client Secret
  • 使用 HTTPS
  • 验证 state 参数(防止 CSRF)
  • 设置合理的 token 过期时间
  • 限制 OAuth App 的权限范围

❌ 不应该做的

  • 将 Client Secret 放在前端代码中
  • 在 URL 中传递敏感信息
  • 忽略 CORS 和同源策略
  • 使用过于宽松的权限范围

7.4 调试技巧

1. 浏览器开发者工具

// Console 中查看 postMessage
window.addEventListener('message', (event) => {
  console.log('收到消息:', event.data);
});

// Network 标签查看请求
// 检查 OAuth 流程中的每个请求

2. 检查 localStorage

// 查看存储的 token
console.log(localStorage.getItem('decap-cms-user'));

// 查看认证状态
console.log(localStorage.getItem('decap-cms.config'));

3. GitHub OAuth App 测试

在 GitHub OAuth App 设置中:
- 查看授权历史
- 检查请求统计
- 重置 Client Secret(如果泄露)

八、总结

8.1 核心要点

  1. OAuth 是授权协议,不是认证协议
  2. 本地环境能工作是因为浏览器的特殊处理和 postMessage 机制
  3. 线上环境需要代理服务器来保护 Client Secret 和解决 CORS
  4. GitHub Pages 的限制导致默认的 Netlify Identity 无法使用
  5. 部署 OAuth 代理服务器是解决问题的关键

8.2 选择建议

  • 生产环境:使用 Vercel OAuth 代理服务器
  • 追求极致性能:使用 Cloudflare Worker
  • 本地开发:使用 local_backend 模式

8.3 学习资源


最后更新时间:2026-01-30

适用场景:Astro + Decap CMS + GitHub Pages 项目