GitHub

用户注册

返回 README

重要:注册是身份层操作。下游 kungal / moyu / wiki 不应在自己前端做注册表单——和改邮箱 / 改密码一样。本文档描述统一的"跳转到 OAuth 注册 → 自动回跳并登录"流程。

整体设计

┌──────────┐                                   ┌─────────────────────┐
│ Kungal   │  ① 用户点"注册" → window.location  │ oauth.kungal.com    │
│ /moyu /  │ ──────────────────────────────►   │ /auth/register      │
│ wiki     │   ?redirect=<encoded(authorize)>   │ ?redirect=...       │
└──────────┘                                   └────────┬────────────┘
     ▲                                                  │
     │                                                  │ ② 填 name/email/password →
     │                                                  │   POST /auth/register/send-code
     │                                                  │   ↓ 后端寄 6 位码到邮箱
     │                                                  │
     │                                                  │ ③ 用户从邮箱取码 → 填进表单 →
     │                                                  │   POST /auth/register { ..., code }
     │                                                  │   (后端验码 + 建账号 +
     │                                                  │    发 access_token + refresh cookie)
     │                                                  ▼
     │                                          ┌─────────────────────┐
     │                                          │ oauth.kungal.com    │
     │                                          │ /oauth/authorize    │
     │                                          │ ?client_id=...      │
     │ ⑥ /auth/callback?code=...               │ &state=...&PKCE=... │
     │ ← exchange → 本站 session 创建            └────────┬────────────┘
     │                                                   │
     │                                                   │ ④ 用户已登录(注册时拿到了 access_token)
     │                                                   │   + client.auto_consent === true
     │                                                   │   ⇒ Container.vue 不渲染同意 UI,
     │                                                   │     直接 POST /oauth/authorize/consent
     │                                                   ▼
     │                                          ┌─────────────────────┐
     │                                          │ oauth backend       │
     │                                          │ issues code         │
     └──────────────────────────────────────────│ 302 → redirect_uri  │
                       ⑤                       └─────────────────────┘

整条链路是注册 + OAuth code 流程合一:用户先通过邮箱验证码证明邮箱所有权,OAuth 后端验码通过后才创建账号并发 token。注册成功后 OAuth web 复用 ?redirect= 直接跳到 /oauth/authorize,由于 client 是第一方(auto_consent=true),同意页跳过,code 立刻发回 kungal,kungal 用现成的 OAuth callback 流程完成本站登录。用户感知是"点注册 → 填表单 → 收码 → 回到原站点已登录",中间 OAuth 域名的存在被淡化到一闪而过。

为什么强制邮箱验证:注册是身份创建动作,邮箱不被验证就建账号 = 任何人都能用别人邮箱去抢注 + 后续找回密码邮件会发到无效地址。两步验证 = 邮箱所有权证明 + 反垃圾注册。同款模式见改邮箱

为什么不在 kungal/moyu 自己前端做注册

身份层政策一致:

  • 唯一身份写入入口:用户表只能从 OAuth 这边写。N 个下游各自实现注册 → N 套验证码 / 限流 / 反爬 / 邮箱去重逻辑 → N 个攻击面
  • 未来收益自动化:将来加 passkey / magic link / 第三方登录 / 异地通知,只改 OAuth 一处,所有下游零代码受益
  • 和登录对齐:登录已经走 OAuth Authorization Code + PKCE(kungal / moyu 已迁移),注册同样走 OAuth 是自然的对称——一个登录入口 + 一个注册入口都在身份提供方
  • 政策一致性:改密码 / 改邮箱必须在 OAuth profile,注册当然也应该在 OAuth

下游唯一要做的是"注册"按钮的跳转 URL —— 和登录按钮共享同一段 PKCE 生成代码即可。


端点

POST /auth/register/send-code

两步注册的第一步:把 6 位数字验证码寄到 email,15 分钟(默认,可通过 KUN_AUTH_VERIFICATION_CODE_TTL_MINUTES 调整)有效。

请求体

{
  "name": "kun",
  "email": "[email protected]"
}
字段 类型 约束
name string 1..17 字符;允许 Unicode 字母/数字 + !~_@#$%^&*()+=-禁止所有不可见 Unicode 字符(零宽、特殊空格、BOM 等 50+ 种)—— 详见 utils.IsValidName
email string 合法邮箱格式

行为

  • 同步预检:name / email 是否已被注册 → 已被注册立即返回错误(不烧验证码额度,不发邮件)
  • 同邮箱限流:15 分钟(默认,可通过 KUN_AUTH_VERIFICATION_CODE_TTL_MINUTES 调整)内同一邮箱最多寄 1 次(Redis key register_code:{email} 兜底)
  • 验证码 6 位数字,存 Redis 15 分钟(默认,可通过 KUN_AUTH_VERIFICATION_CODE_TTL_MINUTES 调整) TTL
  • 调 SMTP([email protected])寄正式邮件,模板见 mail.go SendRegisterCodeEmail

成功响应

{ "code": 0, "message": "验证码已发送到该邮箱", "data": null }

错误响应

HTTP code 触发条件
400 1 JSON 格式错误
400 7 字段约束未通过(name 长度 / email 格式)
400 10006 邮箱已被注册
400 10007 用户名已被使用
400 10012 该邮箱 15 分钟(默认,可通过 KUN_AUTH_VERIFICATION_CODE_TTL_MINUTES 调整)内已发过验证码(限流)
429 同 IP 触发 strict 限流(10/分钟)

错误码 10006 + 10007 会泄露"该邮箱/用户名是否已注册" —— 这是有意权衡:用户体验上需要明确告知"换一个吧",而注册场景的账号枚举攻击面比登录小(登录页只回笼统的"账号或密码错误")。如果产品需要更严的反枚举,未来可改为"如果该邮箱可注册,验证码已寄出"统一文案 + 后端静默吞错。


POST /auth/register

两步注册的第二步:用上一步收到的验证码 + 完整凭证创建账号,并立即发放 token(注册即登录)。

请求体

{
  "name": "kun",
  "email": "[email protected]",
  "password": "secret123",
  "code": "123456"
}
字段 类型 约束
name string 1..17 字符;全局唯一;字符集见 send-code 节(utils.IsValidName
email string 合法邮箱格式;全局唯一;必须与 send-code 时一致(验证码按 email key 存的)
password string 6..100 字符
code string 6 位数字;从 send-code 时寄到 email 的邮件正文中获取

成功响应:返回访问令牌 + 用户资料 + 刷新令牌(写 httpOnly cookie)。

{
  "code": 0,
  "message": "成功",
  "data": {
    "access_token": "eyJhbGc...",
    "user": {
      "uuid": "...",
      "name": "kun",
      "email": "[email protected]",
      "avatar": "",
      "bio": "",
      "moemoepoint": 0,
      "status": 0,
      "roles": [],
      "created_at": "2026-05-23T08:00:00Z"
    }
  }
}

refresh_token 写在 httpOnly cookie 里(Path=/api/v1/auth,7 天),调用方不需要也不应处理。注册成功后 Redis 中的验证码立即被删除,不可重放

错误响应

HTTP code 触发条件
400 1 JSON 格式错误
400 7 字段约束未通过(name/email/password/code 长度或格式)
400 10006 邮箱已被注册(并发竞争;正常 send-code 阶段就该挡掉)
400 10007 用户名已被使用(同上)
400 10010 验证码错误(不匹配 send-code 时存的值)
400 10011 验证码已过期或从未请求(Redis 里没找到这个邮箱的 code)
429 同 IP 触发 strict 限流(10/分钟)

常见 10011 来源:用户改了 email 字段后没重新发验证码 → 后端按新 email 找 Redis key 找不到 → 报"已过期"。前端应当锁定 email 字段直到用户主动点"重新发送"。

调用方只有 oauth.kungal.com 自己的前端应该直接调这两个端点。下游 kungal / moyu / wiki 应该走"跳转到 oauth.kungal.com/auth/register"的模式(见下方"下游接入")。


GET /oauth/client-info

公开元数据查询。无鉴权。供前端在 /oauth/authorize 页面判断是否跳过同意 UI 时调用。

查询参数

参数 必填 说明
client_id OAuth client ID

成功响应

{
  "code": 0,
  "data": {
    "id": "4ed9bc99ec0a789a4796b83e22bd84c5",
    "name": "鲲 Galgame 论坛",
    "auto_consent": true,
    "site_domain": "www.kungal.com"
  }
}
字段 说明
id client_id(回显)
name 展示名
auto_consent 第一方 client 标志;为 true 时前端跳过同意页,直接 POST /oauth/authorize/consent
site_domain 关联 site 的 domain(可空),用于展示"将跳转回 X"

不返回secretredirect_urisscopes 等敏感 / 实现细节。这个端点只为前端判定 UI 行为服务。

错误响应

HTTP code 触发条件
400 2 缺 client_id
404 15001 client_id 不存在

下游接入

1. 注册按钮(kungal / moyu / 任何下游)

登录按钮共用同一段 PKCE 代码,只把目标路径从 /oauth/authorize 换成 /auth/register?redirect=<encoded(/oauth/authorize?...)>

// kungal/apps/web/app/components/register/Register.vue (示意)
const handleOAuthRegister = async () => {
  const codeVerifier = generateCodeVerifier()
  const codeChallenge = await generateCodeChallenge(codeVerifier)
  const state = generateState()

  sessionStorage.setItem('oauth_code_verifier', codeVerifier)
  sessionStorage.setItem('oauth_state', state)

  const authorizeParams = new URLSearchParams({
    client_id: config.public.oauthClientId,
    redirect_uri: config.public.oauthRedirectUri,
    response_type: 'code',
    scope: 'openid profile',
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  })

  // 注册成功后 OAuth web 会跳到这里
  const authorizeUrl = `${config.public.oauthServerUrl}/oauth/authorize?${authorizeParams}`

  // OAuth 注册页 URL;redirect 参数让注册完后串到 authorize 流程
  const registerUrl = `${config.public.oauthWebUrl}/auth/register?redirect=${encodeURIComponent(authorizeUrl)}`

  window.location.href = registerUrl
}

注意 oauthWebUrl 是前端域名(开发 :9420 / 生产 oauth.kungal.com),oauthServerUrl 是 API 域名——两者可能不同。详见 oauth-integration-guide.md §1.3

2. 用户感知的完整时间线

用户看到的 URL 时间 用户感知
1 www.kungal.com/login 点击"注册" 0 ms 点击
2 oauth.kungal.com/auth/register?redirect=... ~200 ms "跳到了账号注册页"
3 同上,填表 用户自主时间 填邮箱 + 密码 + 用户名
4 oauth.kungal.com/oauth/authorize?... ~100 ms(注册返回后立即跳) 白屏一闪(auto_consent 不渲染 UI)
5 www.kungal.com/auth/callback?code=... ~150 ms 白屏一闪(kungal 在交换 token)
6 www.kungal.com/ (或 redirect_uri 配的路径) "我已经登录了"

第 4 步和第 5 步加起来一般在 300 ms 以下,用户感知就是"注册完成后回到原站点已登录"。

3. 已注册用户访问 /auth/register 的处理

用户已登录的状态下访问 oauth.kungal.com/auth/register?redirect=...,OAuth web 应当:

  • 如果 redirect 参数存在 → 立即 window.location.href = redirect(推进 OAuth code 流程)
  • 否则 → 跳 /profile(账号管理页)

绝不应该让已登录用户看到一个空注册表单——会引发"我已经登录了为什么让我再注册一次"的困惑。


oauth_clients.auto_consent boolean,默认 false为 true 表示这个 client 在 /oauth/authorize 流程中跳过用户同意 UI——前端不渲染"该应用将获得以下权限"卡片,直接 POST /oauth/authorize/consent

何时设 true

  • 设 true:第一方 client(owner 是 OAuth 平台自己,比如 kungal / moyu / wiki / AI / sticker)
  • 保持 false:第三方接入应用
  • 保持 false:任何不在你直接控制下的 client

安全模型:auto_consent 不是降低安全等级,是承认"用户已经在使用 kungal,不需要再问一次 kungal 是否可以读他的 OAuth 资料"。这是 SSO 的标准做法——Google 内部应用之间也不会重复问同意。

当前的第一方列表(auto_consent=true):

client_id name site
4ed9bc99ec0a789a4796b83e22bd84c5 鲲 Galgame 论坛 www.kungal.com
df3ff6008d740bfacbe46aa8cf483cf2 鲲 Galgame 补丁 www.moyu.moe
53e9b5ea70bfc4e4d0700a9f7b8818e8 鲲 Galgame Wiki wiki.kungal.com
df46a4cfa71ac919b7b43d63238e2311 鲲 Galgame AI ai.kungal.com
2d8d48a141a3340b43ae206b73cdaa37 鲲 Galgame 表情包 sticker.kungal.com

如果将来接入第三方应用(比如某社区合作伙伴),新建的 client 默认 auto_consent=false,会渲染同意页让用户明确授权——这是 OAuth 协议的正确语义。


未来扩展(L2+)

L2(未来):在 /auth/register/auth/login 页面加 "Continue with Google / GitHub / Apple" 按钮,走标准 OIDC federation。所有下游零代码自动支持——这是 OAuth 集中架构最大的红利。

L3+(待定):passkey / magic link / identifier-first flow 等,都在 OAuth 单点实现,下游不感知。


不在范围内(明确排除)

  • 邀请码 / 内测注册——目前不限制
  • 手机号注册——目前邮箱-only
  • 用户名 vs 邮箱选择——目前 name + email 都必填
  • 二次邮箱验证后才能登录——注册即登录,邮箱验证留给后续防滥用迭代
  • ToS / 隐私政策点击确认——目前没有,加的话只在 OAuth web 这一处加

这些都是 L2+ 议题;L1 只做"注册流程从 legacy 完全迁移到 OAuth 托管"。

源:kun-galgame-infra/docs/integration/oauth/05-registration.md