Skip to content

Authentication System

gxxk-dev edited this page Feb 14, 2026 · 1 revision

认证系统

本文档详细介绍 StudyWithMiku 的用户认证系统,包括 WebAuthn/FIDO2 无密码登录、OAuth 多平台集成、跨账号合并、Token 管理策略和安全措施。


概述

认证系统采用 无密码 设计,支持两种登录方式:

  1. WebAuthn/FIDO2 — 使用设备生物识别(Touch ID/Face ID/Windows Hello)或安全密钥
  2. OAuth 2.0 — 通过 GitHub、Google、Microsoft、LINUX DO 第三方账号登录
flowchart TB
    subgraph Client["浏览器"]
        LoginPanel["AccountLoginPanel"]
        AuthComposable["useAuth()"]
        AuthService["auth.js"]
        AuthStorage["authStorage"]
        WebAuthnHelper["webauthnHelper"]
    end

    subgraph Server["Cloudflare Workers"]
        AuthRoutes["Auth Routes"]
        OAuthRoutes["OAuth Routes"]
        JWTService["JWT Service"]
        WebAuthnService["WebAuthn Service"]
        AuthChallengeDO["AuthChallenge DO"]
        D1["D1 Database"]
    end

    subgraph External["外部服务"]
        GitHub["GitHub"]
        Google["Google"]
        Microsoft["Microsoft"]
        LinuxDo["LINUX DO"]
    end

    LoginPanel --> AuthComposable
    AuthComposable --> AuthService
    AuthComposable --> AuthStorage
    AuthService --> WebAuthnHelper
    AuthService -->|"HTTPS"| AuthRoutes
    AuthService -->|"HTTPS"| OAuthRoutes
    AuthRoutes --> JWTService
    AuthRoutes --> WebAuthnService
    AuthRoutes --> AuthChallengeDO
    AuthRoutes --> D1
    OAuthRoutes -->|"OAuth 2.0"| External
Loading

WebAuthn/FIDO2

注册流程

用户首次注册时通过 WebAuthn 创建凭证,绑定到设备的生物识别或安全密钥。

sequenceDiagram
    participant U as 用户
    participant B as 浏览器
    participant S as 服务端
    participant DO as AuthChallenge DO

    U->>B: 输入用户名,点击注册
    B->>S: POST /auth/register/options {username}
    S->>S: 验证用户名格式和唯一性
    S->>DO: 存储挑战 (5min TTL)
    S-->>B: {challengeId, options}

    B->>B: parseRegisterOptions(options)
    B->>U: 系统弹出生物识别验证
    U->>B: 验证通过
    B->>B: serializeCredential(credential)

    B->>S: POST /auth/register/verify {challengeId, response}
    S->>DO: 获取并删除挑战
    S->>S: verifyRegistration()
    S->>S: 创建用户 + 保存凭证
    S->>S: generateTokenPair()
    S-->>B: {user, tokens} + Set-Cookie: swm_refresh_token
    B->>B: 存储认证状态
Loading

登录流程

sequenceDiagram
    participant U as 用户
    participant B as 浏览器
    participant S as 服务端
    participant DO as AuthChallenge DO

    U->>B: 输入用户名,点击登录
    B->>S: POST /auth/login/options {username}
    S->>DO: 存储挑战 (5min TTL)
    S-->>B: {challengeId, options}

    B->>B: parseLoginOptions(options)
    B->>U: 系统弹出生物识别验证
    U->>B: 验证通过
    B->>B: serializeCredential(credential)

    B->>S: POST /auth/login/verify {challengeId, response}
    S->>DO: 获取并删除挑战
    S->>S: verifyAuthentication()
    S->>S: 更新凭证计数器(克隆检测)
    S->>S: generateTokenPair()
    S-->>B: {user, tokens, warning?}
Loading

设备管理

已登录用户可以添加新设备或移除已有设备:

  • 添加设备: POST /auth/devices/add/optionsPOST /auth/devices/add/verify
  • 删除设备: DELETE /auth/devices/:id(不允许删除最后一个认证方式)
  • 设备名称自动生成: 根据 transportsdeviceType 推断设备类型(如 "内置认证器"、"安全密钥"、"混合认证器")

OAuth 集成

支持的提供商

提供商 Scope 获取信息
GitHub read:user user:email 用户名、头像、邮箱
Google openid email profile 显示名、头像、邮箱
Microsoft openid email profile User.Read 显示名、邮箱
LINUX DO user 用户名、头像

可用的 OAuth 提供商取决于服务端是否配置了对应的 CLIENT_IDCLIENT_SECRET。前端通过 GET /auth/config 查询当前可用的认证方式。

OAuth 登录流程

sequenceDiagram
    participant U as 用户
    participant B as 浏览器
    participant S as 服务端
    participant P as OAuth Provider

    U->>B: 点击 OAuth 按钮
    B->>S: GET /oauth/:provider
    S->>S: 生成 state token
    S-->>B: 302 重定向到 Provider

    B->>P: 授权页面
    U->>P: 确认授权
    P-->>B: 302 重定向到 callback

    B->>S: GET /oauth/:provider/callback?code=...&state=...
    S->>S: 验证 state token
    S->>P: 交换 code 获取 access_token
    S->>P: 获取用户信息
    S->>S: 查找或创建用户
    S->>S: generateTokenPair()
    S-->>B: 302 重定向 #access_token=...&user=...
    B->>B: 解析 URL fragment
    B->>B: 存储认证状态
Loading

OAuth 回调通过 URL fragment#)传递 token,避免 token 出现在服务端日志中。前端解析后立即清除 URL hash。

OAuth 关联流程

已登录用户可以关联额外的 OAuth 账号:

  1. POST /oauth/link/:provider — 获取授权 URL(携带当前用户 ID 的 state)
  2. 重定向到提供商授权
  3. 回调处理时将 OAuth 账号关联到当前用户

如果关联的 OAuth 账号已经存在于另一个用户上,会触发 账号合并流程


跨账号合并

当用户的 WebAuthn 凭证或 OAuth 账号已关联到另一个账号时,系统提供合并机制。

mergeToken 机制

flowchart TB
    Detect["检测到凭证冲突"]
    GenToken["生成 mergeToken (10min TTL)"]
    Store["存储在 AuthChallenge DO"]
    Dialog["弹出合并对话框"]
    Choose["用户选择数据合并策略"]
    Transfer["转移凭证/OAuth 到目标用户"]
    MergeData["按策略合并数据"]
    Cleanup["清理源用户(如无剩余认证方式)"]

    Detect --> GenToken --> Store --> Dialog --> Choose --> Transfer --> MergeData --> Cleanup
Loading

数据合并策略

用户可以为每种数据类型独立选择合并策略:

选项 说明
target 保留目标账号(当前登录)的数据
source 使用源账号的数据
merge 智能合并两个账号的数据

各数据类型的合并逻辑:

数据类型 合并策略
focus_records 按 ID 去重 + 按 updatedAt 做 LWW
playlists 目标优先,按时间戳合并
focus_settings / user_settings LWW by version + deep merge

Token 策略

双 Token 架构

Token 类型 存储位置 有效期 用途
Access Token 内存 (JavaScript 变量) 15 分钟 API 请求授权
Refresh Token HttpOnly Cookie (swm_refresh_token) 7 天 刷新 Access Token

这种分层设计确保:

  • Access Token 不写入任何持久化存储,XSS 攻击无法窃取
  • Refresh Token 通过 HttpOnly + SameSite=Strict Cookie 传输,JavaScript 无法读取
  • 即使 Access Token 泄露,15 分钟后自动失效

Token 刷新机制

sequenceDiagram
    participant B as 浏览器
    participant S as 服务端

    Note over B: Access Token 即将过期 (< 60s)
    B->>S: POST /auth/refresh (Cookie 自动携带)
    S->>S: 验证 Refresh Token
    S->>S: 检查黑名单
    S->>S: 吊销旧 Refresh Token
    S->>S: 生成新 Token Pair
    S-->>B: {accessToken, expiresIn} + Set-Cookie (新 Refresh Token)
    Note over B: 更新内存中的 Access Token
Loading

前端通过定时器(AUTH_CONFIG.TOKEN_CHECK_INTERVAL = 30s)检查 Token 是否即将过期,在过期前 60 秒自动刷新。

Token 黑名单

注销时将 Access Token 和 Refresh Token 的 jti 加入 token_blacklist 表。每次认证请求都会检查黑名单。过期的黑名单条目通过概率性清理(10% 概率)自动移除。


账号管理

用户资料编辑

通过 PATCH /auth/me 更新以下字段:

  • displayName — 显示名称
  • email — 邮箱(用于 Gravatar 头像)
  • qqNumber — QQ 号(用于 QQ 头像)
  • avatarUrl — 自定义头像 URL

多源头像

用户可以从多个来源选择头像:

来源 说明
自动 按优先级自动选择最佳头像
OAuth 使用 OAuth 提供商的头像
Gravatar 基于邮箱 SHA-256 哈希
Libravatar 基于邮箱的开源头像服务
QQ 基于 QQ 号的头像
自定义 用户提供的 URL

UserAvatar.vue 组件按优先级尝试加载,某个来源加载失败会自动回退到下一个。

认证方法管理

GET /auth/methods 返回统一的认证方法列表,包含 WebAuthn 和 OAuth 两类:

// WebAuthn 设备
{ id, type: 'webauthn', deviceName, transports, backedUp, lastUsedAt }

// OAuth 账号
{ id, type: 'oauth', provider, displayName, avatarUrl, email, linkedAt }

删除任何认证方法时会检查是否为最后一个,防止用户锁定自己的账号。


数据库 Schema

认证相关的 4 张表:

erDiagram
    users ||--o{ credentials : "has"
    users ||--o{ oauth_accounts : "has"
    users ||--o{ token_blacklist : "revokes"

    users {
        text id PK
        text username UK
        text displayName
        text avatarUrl
        text email
        text qqNumber
    }

    credentials {
        text id PK
        text userId FK
        blob publicKey
        integer counter
        text transports
        text deviceType
        text deviceName
        integer backedUp
        integer lastUsedAt
    }

    oauth_accounts {
        text id PK
        text userId FK
        text provider
        text providerId
        text displayName
        text avatarUrl
        text email
        integer linkedAt
    }

    token_blacklist {
        text jti PK
        integer expiresAt
    }
Loading

完整的 Schema 定义参见 Backend-Architecture#D1 数据库


安全措施

速率限制

认证相关端点限制为 10 次/分钟(按 IP),通过 RateLimiter Durable Object 实现分布式限制。

克隆检测

WebAuthn 认证时检查凭证的 counter 值。如果新的 counter 没有大于存储的 counter,说明凭证可能被克隆。此时登录仍允许,但响应会携带 warning 字段。

CSRF 防护

  • OAuth state token 绑定到请求会话,回调时验证 state 一致性
  • Refresh Token 使用 SameSite=Strict Cookie,阻止跨站请求
  • 所有认证写操作使用 POST 方法

用户枚举防护

登录时无论用户名是否存在,都返回相同的错误信息,防止攻击者探测有效用户名。

凭证一次性使用

WebAuthn 挑战在验证后立即从 AuthChallenge DO 删除,防止重放攻击。mergeToken 同样在使用后立即销毁。


前端组件参考

Composable

名称 文件 说明
useAuth() src/composables/useAuth.js 认证状态管理(单例),提供登录/注册/OAuth/设备管理等方法

服务与工具

名称 文件 说明
auth service src/services/auth.js 封装所有认证 API 调用
authStorage src/utils/authStorage.js Token/用户信息存储管理
webauthnHelper src/utils/webauthnHelper.js WebAuthn 浏览器 API 封装
oauthProviders src/config/oauthProviders.js OAuth 提供商 UI 元数据

Vue 组件

组件 文件 说明
AccountLoginPanel src/components/settings/tabs/account/AccountLoginPanel.vue 登录/注册面板
AccountProfilePanel src/components/settings/tabs/account/AccountProfilePanel.vue 用户资料编辑
AccountDeviceList src/components/settings/tabs/account/AccountDeviceList.vue 认证方法管理
AccountSyncPanel src/components/settings/tabs/account/AccountSyncPanel.vue 同步状态面板
OAuthButton src/components/settings/tabs/account/OAuthButton.vue OAuth 登录按钮
AccountDeleteConfirm src/components/settings/tabs/account/AccountDeleteConfirm.vue 删除设备确认框
UserAvatar src/components/common/UserAvatar.vue 多源头像组件(优先级回退)

Clone this wiki locally