认证架构设计

要点

  • 一个真正可用的认证系统,不只是“能登录”就够了
  • 在这套方案里,Cloudflare 的各个组件分工应该非常明确,下面我们逐一分析
  • 当前整套设计,其实围绕三条非常关键的原则展开
  • 很多人第一次设计这类系统时,会直觉地想
  • 当前需求里,admin 和 web 的认证边界很明确

内容

1. 概述

接下来要系统介绍登录和鉴权方案,首先我们要明确需求,目标很明确,我们有两个子站,分别是 web 端和 admin 端,他们需要支持不同的登录方式

  • admin 端只支持「邮箱 + 密码」
  • web 端要同时支持「邮箱 + 密码、GitHub OAuth」
  • web 端后续可扩展 Google OAuth
  • web 端登录后可查看、绑定、解绑 GitHub / Google 等账号
  • 认证方案基于 JWT
  • 数据库选择为 Cloudflare D1
  • 高并发下可平滑扩展到 Durable Objects

我们先讲清楚 Cloudflare 生态里的技术选型与整体职责划分,不下沉到表结构和具体接口。

2. 认证系统要解决的问题

一个真正可用的认证系统,不只是“能登录”就够了。admin 端的场景要解决的是角色与权限的绑定问题,而 web 端的场景要解决的是登录方式的绑定问题,以及登录后如何表达当前身份,如何安全刷新凭证,如何绑定、解绑第三方账号等问题。

它至少要同时回答下面这些问题:

  • 用户是谁
  • 用户能用什么方式登录
  • 哪个子站允许哪些登录方式
  • 登录成功后,如何表达当前身份
  • 登录后,如何安全刷新凭证
  • 用户如何绑定、解绑第三方账号
  • 当并发和流量上来后,系统如何继续扩展

如果这些边界一开始没分清,后面就很容易把下面的数据揉在一起:

  • 用户主体
  • 登录方式
  • 角色与权限绑定
  • 会话
  • refresh token
  • 邮箱验证 token
  • 密码重置 token

3. Cloudflare 生态下,各组件的职责划分

在这套方案里,Cloudflare 的各个组件分工应该非常明确,下面我们逐一分析。

3.1 D1 负责认证主数据

D1 是这套认证系统的主数据中心,所有“需要被长期保存、且关系明确”的认证数据,都应该放在这里。

包括:

  • 用户主体
  • 邮箱
  • 密码凭证
  • OAuth 绑定
  • 子站登录策略
  • 角色与权限绑定
  • 会话
  • refresh token
  • 邮箱验证 token
  • 密码重置 token

这些数据有个共同点:

  • 有明确关系
  • 需要事务一致性
  • 需要结构化查询
  • 后面要长期参与登录和鉴权流程,不能丢失

所以它们天然适合放在 D1 里。

3.2 Worker Secrets / Vars 放配置和密钥

有些东西不是业务数据,而是运行时配置或敏感密钥。这些不该放进数据库。

例如:

  • JWT_ACCESS_SECRET
  • JWT_REFRESH_SECRET
  • GITHUB_CLIENT_ID
  • GITHUB_CLIENT_SECRET
  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET
  • APP_URL_WEB
  • APP_URL_ADMIN

这类信息应该放在 Worker Secrets / Vars。

理由很直接:

  • 它们不是业务实体
  • 不需要关系查询
  • 很多值涉及敏感信息
  • 更新方式更偏部署配置,而不是业务写入

3.3 Durable Objects 用于高并发串行控制

Durable Objects 在这套方案里不是一开始就必须上的,但它有一个非常适合的定位:

高并发下的串行控制。

这句话第一次看会有点抽象,可以先把它翻译成一句更容易理解的话:

同一份关键状态,如果短时间内会被很多请求同时修改,就不要让它们一起乱改,而是一个一个排队处理。

这里的关键词有两个:

  • 高并发:同一时刻来了很多请求
  • 串行控制:这些请求不能一起修改同一个关键对象,而是要按顺序一个个处理

为什么认证系统特别需要这种能力?先看一个非常典型的例子。

假设某个用户已经登录,浏览器里拿着一个 refresh token。现在因为网络抖动、页面重复提交,或者用户同时打开了两个标签页,短时间内可能会同时发出两次“刷新 token”的请求。

如果完全不做串行控制,事情就可能变成这样:

  1. 请求 A 进来,读到旧 refresh token 还没被使用
  2. 请求 B 也进来,也读到旧 refresh token 还没被使用
  3. A 认为自己合法,准备签发新 token
  4. B 也认为自己合法,也准备签发新 token
  5. 最后同一个旧 token 被成功使用了两次

这就是并发冲突。认证系统里最怕的,就是同一个敏感凭证被重复消费。

如果加上一层串行控制,流程就会变成:

  1. 请求 A 先进入
  2. 请求 B 先排队等待
  3. A 完成旧 token 校验、标记已使用、发新 token
  4. B 再进来时,发现旧 token 已经被使用,于是直接拒绝

这就是「高并发下的串行控制」。

也就是说,Durable Objects 的价值不在于“又多了一个数据库”,而在于:

  • 同一个 session 的关键操作,能不能保证一次只处理一个
  • 同一个 refresh token,能不能避免被并发消费两次
  • 同一个撤销动作,能不能保证顺序稳定

在认证场景里,最典型的需要串行控制的对象包括:

  • refresh token rotation
  • 单 session 撤销
  • token 重放检测

它们有个共同点:同一个对象在短时间内可能被并发操作,而且顺序非常重要。

3.3.1 为什么 D1 还不够

很多新手看到这里会问:既然 D1 也支持事务,是不是只用 D1 就够了?

大多数情况下,D1 + 事务 是第一步,也是默认方案。这篇并不是说一上来就必须上 Durable Objects。

但当并发很高,而且你特别在意“同一个 session 的所有关键操作必须严格按顺序处理”时,DO 会更合适。

可以把两者理解成:

  • D1:负责把最终状态正确存下来
  • DO:负责让同一个对象的修改操作先排队,再一个个执行

所以它们不是互相替代的关系,而是不同层次的能力。

3.3.2 新手怎么判断什么时候该上 DO

可以先用一个很实用的判断标准。

如果当前系统是下面这种状态:

  • 用户量还不大
  • 登录并发不高
  • refresh token 刷新频率不夸张
  • 还没有明显出现并发刷新重复成功的问题

那么先用 D1 + 事务 就够了。

只有当下面这些情况开始明显出现时,再考虑把关键会话控制迁到 Durable Objects:

  • 同一个 session 高频刷新
  • 并发刷新概率高
  • 对 replay 检测要求很高
  • 需要严格串行的 token rotation

所以这里更准确的结论是:

  • D1 负责把最终状态存下来
  • DO 适合在高并发下做“一次只让一个请求进来处理”的控制层

这样理解之后,就不会把 Durable Objects 误以为“另一个数据库”,也不会误以为“认证系统一上来就必须上 DO”。

3.4 KV 和 R2 不适合承载认证主数据

KV 很适合缓存,但不适合作为认证真相源。

因为认证数据最怕两件事:

  • 延迟一致性
  • 读取到过期状态

只要会话撤销、refresh token 使用状态、账号绑定状态这些信息有概率不一致,认证系统就会出现安全边界问题。

R2 则更不适合放认证结构化数据。它适合对象存储,比如:

  • 用户头像
  • 导出文件
  • 报表附件

但不适合拿来存认证实体和关系数据。

4. 这套认证模型的三个核心原则

当前整套设计,其实围绕三条非常关键的原则展开。

4.1 用户主体和登录方式分离

一个用户以后可能同时具备:

  • 邮箱密码登录
  • GitHub 绑定
  • Google 绑定

所以用户主体必须独立存在,不能把“用户是谁”和“用户怎么登录”绑死在一张表里。

如果一开始就把 GitHub、密码、Google 都揉进同一个 users 表字段里,后面再扩展多登录方式时,结构会很快变乱。

4.2 子站登录规则和用户权限分离

admin 和 web 允许的登录方式不同:

  • admin:password
  • web:password + github
  • 后续 web:password + github + google

这类规则不属于用户本身,而属于“子站策略”。

同样,一个用户有没有 admin 权限,也不等于“这个用户能不能走 GitHub 登录 web”。

所以登录方式策略和权限绑定必须拆开。

4.3 JWT 只做短期 access,refresh 仍然要落库

这套方案不会把所有 token 都做成完全无状态。

更合理的边界是:

  • access token 无状态,短时有效
  • refresh token 有状态,必须落库

这样做的原因很简单。你需要支持:

  • 注销
  • 单设备下线
  • refresh token 轮换
  • replay 检测

这些能力只靠纯无状态 JWT 很难优雅完成。

5. 为什么这里不建议拆两套用户表

很多人第一次设计这类系统时,会直觉地想:

  • admin 搞一套 admin_users
  • web 搞一套 web_users

这个方向看起来很直观,实际问题很多。

因为同一个真实用户未来完全可能同时出现在 web 和 admin 里。再加上 GitHub / Google 绑定能力本来就天然围绕“用户主体”展开,如果拆成两套用户表,后面会马上遇到:

  • 第三方绑定状态要不要同步两边
  • 同一个邮箱在两张表里怎么去重
  • 一个用户既是后台运营又是前台用户时,主体算谁

所以正确边界不是拆双表,而是这样拆:

  • 用户是谁:users
  • 他有哪些登录方式:password_credentialsoauth_identities
  • 哪个子站允许什么方式登录:application_auth_methods
  • 他在各子站有什么权限:user_role_bindings

这比一开始就把人群按子站硬切开要清晰得多。

6. 子站登录规则应该怎么理解

当前需求里,admin 和 web 的认证边界很明确。

6.1 admin

允许:

  • 邮箱 + 密码

不允许:

  • GitHub
  • Google

这意味着 admin 登录时,不只是校验密码对不对,还要同时确认:

  • 当前子站是否允许 password
  • 当前用户是否有 admin 相关角色
  • 当前用户状态是否可用

6.2 web

允许:

  • 邮箱 + 密码
  • GitHub

未来扩展:

  • Google

这意味着 web 登录流程比 admin 多一个外部身份接入层,但这层扩展不应该影响用户主体模型。

也就是说,后面就算再加 Google,本质上也只是给 provider 多加一种值,而不是重做整套认证架构。

7. 先给这套设计一个整体判断

如果把前面的内容收拢起来,这套基于 Cloudflare 生态的认证架构可以概括成下面这组分工:

  • D1:存认证主数据
  • Worker Secrets / Vars:存密钥和部署配置
  • JWT:承载短期 access 身份
  • refresh token:落库并支持轮换与撤销
  • Durable Objects:高并发下的串行控制增强层
  • KV / R2:不做认证真相源

这样设计的好处是:

  • 结构清楚
  • 职责边界稳定
  • 新手能逐层理解
  • 后面扩到 Google、设备管理、会话撤销时不用推翻重来

下一篇会继续把这套设计往下落到数据库模型层,把用户、邮箱、密码凭证、OAuth 绑定、角色、会话、refresh token 这些表一张张拆开讲清楚。