[{"data":1,"prerenderedAt":50},["ShallowReactive",2],{"doc:\u002Fcontracts\u002Foauth\u002F05-registration":3},{"title":4,"route":5,"toc":6,"segments":45,"source":49},"用户注册","\u002Fcontracts\u002Foauth\u002F05-registration",[7,10,13,15,19,22,25,27,30,33,36,39,42],{"id":8,"text":8,"depth":9},"整体设计",2,{"id":11,"text":12,"depth":9},"为什么不在-kungal-moyu-自己前端做注册","为什么不在 kungal\u002Fmoyu 自己前端做注册",{"id":14,"text":14,"depth":9},"端点",{"id":16,"text":17,"depth":18},"post-auth-register-send-code","POST \u002Fauth\u002Fregister\u002Fsend-code",3,{"id":20,"text":21,"depth":18},"post-auth-register","POST \u002Fauth\u002Fregister",{"id":23,"text":24,"depth":18},"get-oauth-client-info","GET \u002Foauth\u002Fclient-info",{"id":26,"text":26,"depth":9},"下游接入",{"id":28,"text":29,"depth":18},"1-注册按钮-kungal-moyu-任何下游","1. 注册按钮（kungal \u002F moyu \u002F 任何下游）",{"id":31,"text":32,"depth":18},"2-用户感知的完整时间线","2. 用户感知的完整时间线",{"id":34,"text":35,"depth":18},"3-已注册用户访问-auth-register-的处理","3. 已注册用户访问 `\u002Fauth\u002Fregister` 的处理",{"id":37,"text":38,"depth":9},"auto_consent-字段语义","auto_consent 字段语义",{"id":40,"text":41,"depth":9},"未来扩展-l2","未来扩展（L2+）",{"id":43,"text":44,"depth":9},"不在范围内-明确排除","不在范围内（明确排除）",[46],{"type":47,"html":48},"html","\u003Ch1 id=\"用户注册\" tabindex=\"-1\">用户注册\u003C\u002Fh1>\n\u003Cp>返回 \u003Ca href=\"\u002Fcontracts\u002Foauth\">README\u003C\u002Fa>\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>重要：注册是身份层操作。下游 kungal \u002F moyu \u002F wiki 不应在自己前端做注册表单\u003C\u002Fstrong>——和\u003Ca href=\"\u002Fcontracts\u002Foauth\u002F02-user-profile#%E8%BA%AB%E4%BB%BD%E6%93%8D%E4%BD%9C-vs-%E5%B1%95%E7%A4%BA%E6%93%8D%E4%BD%9C\">改邮箱 \u002F 改密码一样\u003C\u002Fa>。本文档描述统一的&quot;跳转到 OAuth 注册 → 自动回跳并登录&quot;流程。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"整体设计\" tabindex=\"-1\">整体设计\u003C\u002Fh2>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode class=\"language-text\">\u003Cspan class=\"line\">\u003Cspan>┌──────────┐                                   ┌─────────────────────┐\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>│ Kungal   │  ① 用户点\"注册\" → window.location  │ oauth.kungal.com    │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>│ \u002Fmoyu \u002F  │ ──────────────────────────────►   │ \u002Fauth\u002Fregister      │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>│ wiki     │   ?redirect=&#x3C;encoded(authorize)>   │ ?redirect=...       │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>└──────────┘                                   └────────┬────────────┘\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     ▲                                                  │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  │ ② 填 name\u002Femail\u002Fpassword →\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  │   POST \u002Fauth\u002Fregister\u002Fsend-code\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  │   ↓ 后端寄 6 位码到邮箱\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  │ ③ 用户从邮箱取码 → 填进表单 →\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  │   POST \u002Fauth\u002Fregister { ..., code }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  │   (后端验码 + 建账号 +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  │    发 access_token + refresh cookie)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                  ▼\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                          ┌─────────────────────┐\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                          │ oauth.kungal.com    │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                          │ \u002Foauth\u002Fauthorize    │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                          │ ?client_id=...      │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │ ⑥ \u002Fauth\u002Fcallback?code=...               │ &#x26;state=...&#x26;PKCE=... │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │ ← exchange → 本站 session 创建            └────────┬────────────┘\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                   │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                   │ ④ 用户已登录（注册时拿到了 access_token）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                   │   + client.auto_consent === true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                   │   ⇒ Container.vue 不渲染同意 UI，\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                   │     直接 POST \u002Foauth\u002Fauthorize\u002Fconsent\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                                   ▼\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                          ┌─────────────────────┐\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                          │ oauth backend       │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     │                                          │ issues code         │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     └──────────────────────────────────────────│ 302 → redirect_uri  │\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>                       ⑤                       └─────────────────────┘\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>整条链路是\u003Cstrong>注册 + OAuth code 流程合一\u003C\u002Fstrong>：用户先通过邮箱验证码证明邮箱所有权，OAuth 后端验码通过后才创建账号并发 token。注册成功后 OAuth web 复用 \u003Ccode>?redirect=\u003C\u002Fcode> 直接跳到 \u003Ccode>\u002Foauth\u002Fauthorize\u003C\u002Fcode>，由于 client 是第一方（\u003Ccode>auto_consent=true\u003C\u002Fcode>），同意页跳过，code 立刻发回 kungal，kungal 用现成的 OAuth callback 流程完成本站登录。\u003Cstrong>用户感知是&quot;点注册 → 填表单 → 收码 → 回到原站点已登录&quot;\u003C\u002Fstrong>，中间 OAuth 域名的存在被淡化到一闪而过。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>\u003Cstrong>为什么强制邮箱验证\u003C\u002Fstrong>：注册是身份创建动作，邮箱不被验证就建账号 = 任何人都能用别人邮箱去抢注 + 后续找回密码邮件会发到无效地址。两步验证 = 邮箱所有权证明 + 反垃圾注册。同款模式见\u003Ca href=\"\u002Fcontracts\u002Foauth\u002F02-user-profile#post-authemailsend-code\">改邮箱\u003C\u002Fa>。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"为什么不在-kungal-moyu-自己前端做注册\" tabindex=\"-1\">为什么不在 kungal\u002Fmoyu 自己前端做注册\u003C\u002Fh2>\n\u003Cp>和\u003Ca href=\"\u002Fcontracts\u002Foauth\u002F02-user-profile#%E8%BA%AB%E4%BB%BD%E6%93%8D%E4%BD%9C-vs-%E5%B1%95%E7%A4%BA%E6%93%8D%E4%BD%9C\">身份层政策\u003C\u002Fa>一致：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>唯一身份写入入口\u003C\u002Fstrong>：用户表只能从 OAuth 这边写。N 个下游各自实现注册 → N 套验证码 \u002F 限流 \u002F 反爬 \u002F 邮箱去重逻辑 → N 个攻击面\u003C\u002Fli>\n\u003Cli>\u003Cstrong>未来收益自动化\u003C\u002Fstrong>：将来加 passkey \u002F magic link \u002F 第三方登录 \u002F 异地通知，只改 OAuth 一处，所有下游零代码受益\u003C\u002Fli>\n\u003Cli>\u003Cstrong>和登录对齐\u003C\u002Fstrong>：登录已经走 OAuth Authorization Code + PKCE（kungal \u002F moyu 已迁移），注册同样走 OAuth 是自然的对称——一个登录入口 + 一个注册入口都在身份提供方\u003C\u002Fli>\n\u003Cli>\u003Cstrong>政策一致性\u003C\u002Fstrong>：改密码 \u002F 改邮箱必须在 OAuth profile，注册当然也应该在 OAuth\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>下游唯一要做的是&quot;注册&quot;按钮的跳转 URL —— 和登录按钮共享同一段 PKCE 生成代码即可。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"端点\" tabindex=\"-1\">端点\u003C\u002Fh2>\n\u003Ch3 id=\"post-auth-register-send-code\" tabindex=\"-1\">POST \u002Fauth\u002Fregister\u002Fsend-code\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>两步注册的第一步\u003C\u002Fstrong>：把 6 位数字验证码寄到 \u003Ccode>email\u003C\u002Fcode>，15 分钟（默认，可通过 \u003Ccode>KUN_AUTH_VERIFICATION_CODE_TTL_MINUTES\u003C\u002Fcode> 调整）有效。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>请求体\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode class=\"language-json\">\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"name\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"kun\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"email\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"kun@kungal.com\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>字段\u003C\u002Fth>\n\u003Cth>类型\u003C\u002Fth>\n\u003Cth>约束\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>name\u003C\u002Ftd>\n\u003Ctd>string\u003C\u002Ftd>\n\u003Ctd>1..17 字符；允许 Unicode 字母\u002F数字 + \u003Ccode>!~_@#$%^&amp;*()+=-\u003C\u002Fcode>；\u003Cstrong>禁止\u003C\u002Fstrong>所有不可见 Unicode 字符（零宽、特殊空格、BOM 等 50+ 种）—— 详见 \u003Ccode>utils.IsValidName\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>email\u003C\u002Ftd>\n\u003Ctd>string\u003C\u002Ftd>\n\u003Ctd>合法邮箱格式\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>\u003Cstrong>行为\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>同步预检：name \u002F email 是否已被注册 → 已被注册立即返回错误（不烧验证码额度，不发邮件）\u003C\u002Fli>\n\u003Cli>同邮箱限流：15 分钟（默认，可通过 \u003Ccode>KUN_AUTH_VERIFICATION_CODE_TTL_MINUTES\u003C\u002Fcode> 调整）内同一邮箱最多寄 1 次（Redis key \u003Ccode>register_code:{email}\u003C\u002Fcode> 兜底）\u003C\u002Fli>\n\u003Cli>验证码 6 位数字，存 Redis 15 分钟（默认，可通过 \u003Ccode>KUN_AUTH_VERIFICATION_CODE_TTL_MINUTES\u003C\u002Fcode> 调整） TTL\u003C\u002Fli>\n\u003Cli>调 SMTP（\u003Ccode>auth@kungal.com\u003C\u002Fcode>）寄正式邮件，模板见 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FKunMoe\u002Fkun-galgame-infra\u002Fblob\u002Fmain\u002Fapps\u002Fapi\u002Finternal\u002Finfrastructure\u002Fmail\u002Fmail.go\">\u003Ccode>mail.go SendRegisterCodeEmail\u003C\u002Fcode>\u003C\u002Fa>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>成功响应\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode class=\"language-json\">\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">{ \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\"code\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\"message\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"验证码已发送到该邮箱\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\"data\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>错误响应\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>HTTP\u003C\u002Fth>\n\u003Cth>code\u003C\u002Fth>\n\u003Cth>触发条件\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>1\u003C\u002Ftd>\n\u003Ctd>JSON 格式错误\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>7\u003C\u002Ftd>\n\u003Ctd>字段约束未通过（name 长度 \u002F email 格式）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>10006\u003C\u002Ftd>\n\u003Ctd>邮箱已被注册\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>10007\u003C\u002Ftd>\n\u003Ctd>用户名已被使用\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>10012\u003C\u002Ftd>\n\u003Ctd>该邮箱 15 分钟（默认，可通过 \u003Ccode>KUN_AUTH_VERIFICATION_CODE_TTL_MINUTES\u003C\u002Fcode> 调整）内已发过验证码（限流）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>429\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>同 IP 触发 strict 限流（10\u002F分钟）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cblockquote>\n\u003Cp>\u003Cstrong>错误码 10006 + 10007 会泄露&quot;该邮箱\u002F用户名是否已注册&quot;\u003C\u002Fstrong> —— 这是有意权衡：用户体验上需要明确告知&quot;换一个吧&quot;，而注册场景的账号枚举攻击面比登录小（登录页只回笼统的&quot;账号或密码错误&quot;）。如果产品需要更严的反枚举，未来可改为&quot;如果该邮箱可注册，验证码已寄出&quot;统一文案 + 后端静默吞错。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Chr>\n\u003Ch3 id=\"post-auth-register\" tabindex=\"-1\">POST \u002Fauth\u002Fregister\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>两步注册的第二步\u003C\u002Fstrong>：用上一步收到的验证码 + 完整凭证创建账号，并立即发放 token（\u003Cstrong>注册即登录\u003C\u002Fstrong>）。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>请求体\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode class=\"language-json\">\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"name\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"kun\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"email\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"kun@kungal.com\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"password\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"secret123\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"code\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"123456\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>字段\u003C\u002Fth>\n\u003Cth>类型\u003C\u002Fth>\n\u003Cth>约束\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>name\u003C\u002Ftd>\n\u003Ctd>string\u003C\u002Ftd>\n\u003Ctd>1..17 字符；全局唯一；字符集见 send-code 节（\u003Ccode>utils.IsValidName\u003C\u002Fcode>）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>email\u003C\u002Ftd>\n\u003Ctd>string\u003C\u002Ftd>\n\u003Ctd>合法邮箱格式；全局唯一；\u003Cstrong>必须与 send-code 时一致\u003C\u002Fstrong>（验证码按 email key 存的）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>password\u003C\u002Ftd>\n\u003Ctd>string\u003C\u002Ftd>\n\u003Ctd>6..100 字符\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>code\u003C\u002Ftd>\n\u003Ctd>string\u003C\u002Ftd>\n\u003Ctd>6 位数字；从 send-code 时寄到 email 的邮件正文中获取\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>\u003Cstrong>成功响应\u003C\u002Fstrong>：返回访问令牌 + 用户资料 + 刷新令牌（写 httpOnly cookie）。\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode class=\"language-json\">\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"code\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"message\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"成功\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"data\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"access_token\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"eyJhbGc...\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"user\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"uuid\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"...\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"name\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"kun\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"email\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"kun@kungal.com\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"avatar\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"bio\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"moemoepoint\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"status\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"roles\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">      \"created_at\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2026-05-23T08:00:00Z\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>refresh_token\u003C\u002Fcode> 写在 httpOnly cookie 里（\u003Ccode>Path=\u002Fapi\u002Fv1\u002Fauth\u003C\u002Fcode>，7 天），调用方不需要也不应处理。注册成功后 Redis 中的验证码立即被删除，\u003Cstrong>不可重放\u003C\u002Fstrong>。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>错误响应\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>HTTP\u003C\u002Fth>\n\u003Cth>code\u003C\u002Fth>\n\u003Cth>触发条件\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>1\u003C\u002Ftd>\n\u003Ctd>JSON 格式错误\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>7\u003C\u002Ftd>\n\u003Ctd>字段约束未通过（name\u002Femail\u002Fpassword\u002Fcode 长度或格式）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>10006\u003C\u002Ftd>\n\u003Ctd>邮箱已被注册（并发竞争；正常 send-code 阶段就该挡掉）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>10007\u003C\u002Ftd>\n\u003Ctd>用户名已被使用（同上）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>10010\u003C\u002Ftd>\n\u003Ctd>验证码错误（不匹配 send-code 时存的值）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>10011\u003C\u002Ftd>\n\u003Ctd>验证码已过期或从未请求（Redis 里没找到这个邮箱的 code）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>429\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>同 IP 触发 strict 限流（10\u002F分钟）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cblockquote>\n\u003Cp>\u003Cstrong>常见 10011 来源\u003C\u002Fstrong>：用户改了 email 字段后没重新发验证码 → 后端按新 email 找 Redis key 找不到 → 报&quot;已过期&quot;。前端应当锁定 email 字段直到用户主动点&quot;重新发送&quot;。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>\u003Cstrong>调用方\u003C\u002Fstrong>：\u003Cstrong>只有 oauth.kungal.com 自己的前端应该直接调这两个端点\u003C\u002Fstrong>。下游 kungal \u002F moyu \u002F wiki 应该走&quot;跳转到 oauth.kungal.com\u002Fauth\u002Fregister&quot;的模式（见下方&quot;下游接入&quot;）。\u003C\u002Fp>\n\u003Chr>\n\u003Ch3 id=\"get-oauth-client-info\" tabindex=\"-1\">GET \u002Foauth\u002Fclient-info\u003C\u002Fh3>\n\u003Cp>公开元数据查询。无鉴权。供前端在 \u003Ccode>\u002Foauth\u002Fauthorize\u003C\u002Fcode> 页面\u003Cstrong>判断是否跳过同意 UI\u003C\u002Fstrong> 时调用。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>查询参数\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>参数\u003C\u002Fth>\n\u003Cth>必填\u003C\u002Fth>\n\u003Cth>说明\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>client_id\u003C\u002Ftd>\n\u003Ctd>是\u003C\u002Ftd>\n\u003Ctd>OAuth client ID\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>\u003Cstrong>成功响应\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode class=\"language-json\">\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"code\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"data\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"id\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"4ed9bc99ec0a789a4796b83e22bd84c5\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"name\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"鲲 Galgame 论坛\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"auto_consent\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">true\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"site_domain\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"www.kungal.com\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>字段\u003C\u002Fth>\n\u003Cth>说明\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>id\u003C\u002Ftd>\n\u003Ctd>client_id（回显）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>name\u003C\u002Ftd>\n\u003Ctd>展示名\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>auto_consent\u003C\u002Ftd>\n\u003Ctd>第一方 client 标志；为 true 时前端\u003Cstrong>跳过同意页\u003C\u002Fstrong>，直接 POST \u003Ccode>\u002Foauth\u002Fauthorize\u002Fconsent\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>site_domain\u003C\u002Ftd>\n\u003Ctd>关联 site 的 domain（可空），用于展示&quot;将跳转回 X&quot;\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cblockquote>\n\u003Cp>\u003Cstrong>不返回\u003C\u002Fstrong>：\u003Ccode>secret\u003C\u002Fcode>、\u003Ccode>redirect_uris\u003C\u002Fcode>、\u003Ccode>scopes\u003C\u002Fcode> 等敏感 \u002F 实现细节。这个端点只为前端判定 UI 行为服务。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>\u003Cstrong>错误响应\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>HTTP\u003C\u002Fth>\n\u003Cth>code\u003C\u002Fth>\n\u003Cth>触发条件\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>400\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>缺 client_id\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>404\u003C\u002Ftd>\n\u003Ctd>15001\u003C\u002Ftd>\n\u003Ctd>client_id 不存在\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Chr>\n\u003Ch2 id=\"下游接入\" tabindex=\"-1\">下游接入\u003C\u002Fh2>\n\u003Ch3 id=\"1-注册按钮-kungal-moyu-任何下游\" tabindex=\"-1\">1. 注册按钮（kungal \u002F moyu \u002F 任何下游）\u003C\u002Fh3>\n\u003Cp>和\u003Cstrong>登录按钮共用同一段 PKCE 代码\u003C\u002Fstrong>，只把目标路径从 \u003Ccode>\u002Foauth\u002Fauthorize\u003C\u002Fcode> 换成 \u003Ccode>\u002Fauth\u002Fregister?redirect=&lt;encoded(\u002Foauth\u002Fauthorize?...)&gt;\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode class=\"language-ts\">\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F kungal\u002Fapps\u002Fweb\u002Fapp\u002Fcomponents\u002Fregister\u002FRegister.vue (示意)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">const\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handleOAuthRegister\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> async\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=>\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> codeVerifier\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> generateCodeVerifier\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> codeChallenge\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> await\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> generateCodeChallenge\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(codeVerifier)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> state\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> generateState\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">  sessionStorage.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setItem\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'oauth_code_verifier'\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, codeVerifier)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">  sessionStorage.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setItem\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'oauth_state'\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, state)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> authorizeParams\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> URLSearchParams\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">({\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    client_id: config.public.oauthClientId,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    redirect_uri: config.public.oauthRedirectUri,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    response_type: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'code'\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    scope: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'openid profile'\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    state,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    code_challenge: codeChallenge,\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    code_challenge_method: \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">'S256'\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">  })\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">  \u002F\u002F 注册成功后 OAuth web 会跳到这里\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> authorizeUrl\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `${\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">config\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">.\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">public\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">.\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">oauthServerUrl\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\u002Foauth\u002Fauthorize?${\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">authorizeParams\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">  \u002F\u002F OAuth 注册页 URL；redirect 参数让注册完后串到 authorize 流程\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">  const\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> registerUrl\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> =\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `${\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">config\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">.\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">public\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">.\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">oauthWebUrl\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\u002Fauth\u002Fregister?redirect=${\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">encodeURIComponent\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">(\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">authorizeUrl\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">  window.location.href \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> registerUrl\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意 \u003Ccode>oauthWebUrl\u003C\u002Fcode> 是前端域名（开发 \u003Ccode>:9420\u003C\u002Fcode> \u002F 生产 \u003Ccode>oauth.kungal.com\u003C\u002Fcode>），\u003Ccode>oauthServerUrl\u003C\u002Fcode> 是 API 域名——两者可能不同。详见 \u003Ca href=\"\u002Fcontracts\u002Foauth\u002Foauth-integration-guide#13-oauth-server-%E5%9C%B0%E5%9D%80\">oauth-integration-guide.md §1.3\u003C\u002Fa>。\u003C\u002Fp>\n\u003Ch3 id=\"2-用户感知的完整时间线\" tabindex=\"-1\">2. 用户感知的完整时间线\u003C\u002Fh3>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>步\u003C\u002Fth>\n\u003Cth>用户看到的 URL\u003C\u002Fth>\n\u003Cth>时间\u003C\u002Fth>\n\u003Cth>用户感知\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>1\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>www.kungal.com\u002Flogin\u003C\u002Fcode> 点击&quot;注册&quot;\u003C\u002Ftd>\n\u003Ctd>0 ms\u003C\u002Ftd>\n\u003Ctd>点击\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>oauth.kungal.com\u002Fauth\u002Fregister?redirect=...\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>~200 ms\u003C\u002Ftd>\n\u003Ctd>&quot;跳到了账号注册页&quot;\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>3\u003C\u002Ftd>\n\u003Ctd>同上，填表\u003C\u002Ftd>\n\u003Ctd>用户自主时间\u003C\u002Ftd>\n\u003Ctd>填邮箱 + 密码 + 用户名\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>4\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>oauth.kungal.com\u002Foauth\u002Fauthorize?...\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>~100 ms（注册返回后立即跳）\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>白屏一闪\u003C\u002Fstrong>（auto_consent 不渲染 UI）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>5\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>www.kungal.com\u002Fauth\u002Fcallback?code=...\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>~150 ms\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>白屏一闪\u003C\u002Fstrong>（kungal 在交换 token）\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>6\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>www.kungal.com\u002F\u003C\u002Fcode> (或 redirect_uri 配的路径)\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>&quot;我已经登录了&quot;\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>第 4 步和第 5 步加起来一般在 300 ms 以下，用户感知就是&quot;注册完成后回到原站点已登录&quot;。\u003C\u002Fp>\n\u003Ch3 id=\"3-已注册用户访问-auth-register-的处理\" tabindex=\"-1\">3. 已注册用户访问 \u003Ccode>\u002Fauth\u002Fregister\u003C\u002Fcode> 的处理\u003C\u002Fh3>\n\u003Cp>用户已登录的状态下访问 \u003Ccode>oauth.kungal.com\u002Fauth\u002Fregister?redirect=...\u003C\u002Fcode>，OAuth web 应当：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>如果 \u003Ccode>redirect\u003C\u002Fcode> 参数存在 → 立即 \u003Ccode>window.location.href = redirect\u003C\u002Fcode>（推进 OAuth code 流程）\u003C\u002Fli>\n\u003Cli>否则 → 跳 \u003Ccode>\u002Fprofile\u003C\u002Fcode>（账号管理页）\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>绝不应该\u003C\u002Fstrong>让已登录用户看到一个空注册表单——会引发&quot;我已经登录了为什么让我再注册一次&quot;的困惑。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"auto_consent-字段语义\" tabindex=\"-1\">auto_consent 字段语义\u003C\u002Fh2>\n\u003Cp>\u003Ccode>oauth_clients.auto_consent\u003C\u002Fcode> boolean，默认 \u003Ccode>false\u003C\u002Fcode>。\u003Cstrong>为 true 表示这个 client 在 \u003Ccode>\u002Foauth\u002Fauthorize\u003C\u002Fcode> 流程中跳过用户同意 UI\u003C\u002Fstrong>——前端不渲染&quot;该应用将获得以下权限&quot;卡片，直接 POST \u003Ccode>\u002Foauth\u002Fauthorize\u002Fconsent\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>何时设 true\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>设 true\u003C\u002Fstrong>：第一方 client（owner 是 OAuth 平台自己，比如 kungal \u002F moyu \u002F wiki \u002F AI \u002F sticker）\u003C\u002Fli>\n\u003Cli>\u003Cstrong>保持 false\u003C\u002Fstrong>：第三方接入应用\u003C\u002Fli>\n\u003Cli>\u003Cstrong>保持 false\u003C\u002Fstrong>：任何不在你直接控制下的 client\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>安全模型\u003C\u002Fstrong>：auto_consent 不是降低安全等级，是承认&quot;用户已经在使用 kungal，不需要再问一次 kungal 是否可以读他的 OAuth 资料&quot;。这是 SSO 的标准做法——Google 内部应用之间也不会重复问同意。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>当前的第一方列表\u003C\u002Fstrong>（auto_consent=true）：\u003C\u002Fp>\n\u003Cdiv class=\"kun-table-wrap\">\u003Ctable>\u003Cthead>\n\u003Ctr>\n\u003Cth>client_id\u003C\u002Fth>\n\u003Cth>name\u003C\u002Fth>\n\u003Cth>site\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>4ed9bc99ec0a789a4796b83e22bd84c5\u003C\u002Ftd>\n\u003Ctd>鲲 Galgame 论坛\u003C\u002Ftd>\n\u003Ctd>www.kungal.com\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>df3ff6008d740bfacbe46aa8cf483cf2\u003C\u002Ftd>\n\u003Ctd>鲲 Galgame 补丁\u003C\u002Ftd>\n\u003Ctd>www.moyu.moe\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>53e9b5ea70bfc4e4d0700a9f7b8818e8\u003C\u002Ftd>\n\u003Ctd>鲲 Galgame Wiki\u003C\u002Ftd>\n\u003Ctd>wiki.kungal.com\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>df46a4cfa71ac919b7b43d63238e2311\u003C\u002Ftd>\n\u003Ctd>鲲 Galgame AI\u003C\u002Ftd>\n\u003Ctd>ai.kungal.com\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>2d8d48a141a3340b43ae206b73cdaa37\u003C\u002Ftd>\n\u003Ctd>鲲 Galgame 表情包\u003C\u002Ftd>\n\u003Ctd>sticker.kungal.com\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\u003Cp>如果将来接入第三方应用（比如某社区合作伙伴），新建的 client 默认 \u003Ccode>auto_consent=false\u003C\u002Fcode>，会渲染同意页让用户明确授权——这是 OAuth 协议的正确语义。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"未来扩展-l2\" tabindex=\"-1\">未来扩展（L2+）\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>L2（未来）\u003C\u002Fstrong>：在 \u003Ccode>\u002Fauth\u002Fregister\u003C\u002Fcode> 和 \u003Ccode>\u002Fauth\u002Flogin\u003C\u002Fcode> 页面加 &quot;Continue with Google \u002F GitHub \u002F Apple&quot; 按钮，走标准 OIDC federation。\u003Cstrong>所有下游零代码自动支持\u003C\u002Fstrong>——这是 OAuth 集中架构最大的红利。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>L3+（待定）\u003C\u002Fstrong>：passkey \u002F magic link \u002F identifier-first flow 等，都在 OAuth 单点实现，下游不感知。\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"不在范围内-明确排除\" tabindex=\"-1\">不在范围内（明确排除）\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>邀请码 \u002F 内测注册——目前不限制\u003C\u002Fli>\n\u003Cli>手机号注册——目前邮箱-only\u003C\u002Fli>\n\u003Cli>用户名 vs 邮箱选择——目前 name + email 都必填\u003C\u002Fli>\n\u003Cli>二次邮箱验证后才能登录——注册即登录，邮箱验证留给后续防滥用迭代\u003C\u002Fli>\n\u003Cli>ToS \u002F 隐私政策点击确认——目前没有，加的话只在 OAuth web 这一处加\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>这些都是 L2+ 议题；L1 只做&quot;注册流程从 legacy 完全迁移到 OAuth 托管&quot;。\u003C\u002Fp>\n","kun-galgame-infra\u002Fdocs\u002Fintegration\u002Foauth\u002F05-registration.md",1781708341787]