认证架构设计
要点
- 一个真正可用的认证系统,不只是“能登录”就够了
- 在这套方案里,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_SECRETJWT_REFRESH_SECRETGITHUB_CLIENT_IDGITHUB_CLIENT_SECRETGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETAPP_URL_WEBAPP_URL_ADMIN
这类信息应该放在 Worker Secrets / Vars。
理由很直接:
- 它们不是业务实体
- 不需要关系查询
- 很多值涉及敏感信息
- 更新方式更偏部署配置,而不是业务写入
3.3 Durable Objects 用于高并发串行控制
Durable Objects 在这套方案里不是一开始就必须上的,但它有一个非常适合的定位:
高并发下的串行控制。
这句话第一次看会有点抽象,可以先把它翻译成一句更容易理解的话:
同一份关键状态,如果短时间内会被很多请求同时修改,就不要让它们一起乱改,而是一个一个排队处理。
这里的关键词有两个:
- 高并发:同一时刻来了很多请求
- 串行控制:这些请求不能一起修改同一个关键对象,而是要按顺序一个个处理
为什么认证系统特别需要这种能力?先看一个非常典型的例子。
假设某个用户已经登录,浏览器里拿着一个 refresh token。现在因为网络抖动、页面重复提交,或者用户同时打开了两个标签页,短时间内可能会同时发出两次“刷新 token”的请求。
如果完全不做串行控制,事情就可能变成这样:
- 请求 A 进来,读到旧 refresh token 还没被使用
- 请求 B 也进来,也读到旧 refresh token 还没被使用
- A 认为自己合法,准备签发新 token
- B 也认为自己合法,也准备签发新 token
- 最后同一个旧 token 被成功使用了两次
这就是并发冲突。认证系统里最怕的,就是同一个敏感凭证被重复消费。
如果加上一层串行控制,流程就会变成:
- 请求 A 先进入
- 请求 B 先排队等待
- A 完成旧 token 校验、标记已使用、发新 token
- 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_credentials、oauth_identities - 哪个子站允许什么方式登录:
application_auth_methods - 他在各子站有什么权限:
user_role_bindings
这比一开始就把人群按子站硬切开要清晰得多。
6. 子站登录规则应该怎么理解
当前需求里,admin 和 web 的认证边界很明确。
6.1 admin
允许:
- 邮箱 + 密码
不允许:
- GitHub
这意味着 admin 登录时,不只是校验密码对不对,还要同时确认:
- 当前子站是否允许
password - 当前用户是否有 admin 相关角色
- 当前用户状态是否可用
6.2 web
允许:
- 邮箱 + 密码
- GitHub
未来扩展:
这意味着 web 登录流程比 admin 多一个外部身份接入层,但这层扩展不应该影响用户主体模型。
也就是说,后面就算再加 Google,本质上也只是给 provider 多加一种值,而不是重做整套认证架构。
7. 先给这套设计一个整体判断
如果把前面的内容收拢起来,这套基于 Cloudflare 生态的认证架构可以概括成下面这组分工:
- D1:存认证主数据
- Worker Secrets / Vars:存密钥和部署配置
- JWT:承载短期 access 身份
- refresh token:落库并支持轮换与撤销
- Durable Objects:高并发下的串行控制增强层
- KV / R2:不做认证真相源
这样设计的好处是:
- 结构清楚
- 职责边界稳定
- 新手能逐层理解
- 后面扩到 Google、设备管理、会话撤销时不用推翻重来
下一篇会继续把这套设计往下落到数据库模型层,把用户、邮箱、密码凭证、OAuth 绑定、角色、会话、refresh token 这些表一张张拆开讲清楚。