Files
AIC-Project/docs/superpowers/specs/2026-06-24-account-auth-design.md
T
2026-07-10 10:24:29 +08:00

9.1 KiB
Raw Blame History

账号鉴权(真登录 + 签名 token)设计

日期:2026-06-24 状态:已与用户确认设计,待写实施计划

背景与目标

当前网关 /ws?pid=<int> 直接信任客户端传来的明文 pidServer/Gateway/GameServerHost.cs:49-77),pid 是客户端对账号做 FNV 哈希得到(CSharpClientApp.GetStablePlayerId)。后果:任何人可在 URL 里填任意 pid 冒充任意玩家、劫持会话(SessionManager 以 pid 为键,同 pid 视为重连)。这是公网上线的硬安全缺口(memory 多处记为「未做鉴权」)。

目标:引入真账号体系——账号+密码(PBKDF2 哈希)验证后签发 HMAC 签名 token,网关用 token 验签取权威 pid,杜绝伪造 pid / 冒充。

范围(用户已定)

  • 支持:注册新账号(开放注册)、账号密码登录
  • 不做:游客登录、邀请码。
  • 账号存 SQLite 单文件库
  • token 用 HMAC 签名、无状态(不易主动吊销,靠过期;将来需吊销再叠加会话表)。

非目标(本期不做,列为后续):主动吊销/踢人、限流(先靠 Caddy)、找回密码、多设备管理、邮箱/手机验证。

架构

新增隔离子系统 Server/Auth/(库 XWorld.Server.Authnet10.0netstandard 不要求),仅在连接/登录时被调用,不进 tick 热路径,与单线程 tick actor 兼容。

客户端                              Caddy(TLS)            网关 Kestrel (:5005)
  │  POST /login {user,pwd} ──HTTPS──▶ game.xworld.link ──▶ AuthEndpoints
  │  ◀────────── {token, pid} ────────────────────────────  └─ AccountStore.Verify → TokenService.Issue
  │
  │  wss://…/ws?token=<token> ─WSS──▶ ─────────────────────▶ /ws 中间件
  │                                                           └─ TokenService.Verify → pid → ConnectCommand

组件与职责

AccountStoreSQLite

  • 依赖 Microsoft.Data.Sqlite(新增 NuGet)。
  • 表:accounts(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL COLLATE NOCASE, pwd_hash BLOB NOT NULL, salt BLOB NOT NULL, iter INTEGER NOT NULL, created_at INTEGER NOT NULL)
  • int CreateAccount(string username, string password) → 新账号 id;用户名重复抛/返回明确错误(映射 409)。
  • int? VerifyCredentials(string username, string password) → 命中且密码正确返回 id,否则 null。
  • 密码哈希:Rfc2898DeriveBytesPBKDF2-SHA256),每用户 16 字节随机盐,迭代数 ≥100000(存 iter 便于将来升级),输出 32 字节。比较用定长 CryptographicOperations.FixedTimeEquals
  • 连接:单文件 accounts.db,路径由网关参数 --authDbPath 指定;开 WALAccountStore 内部串行化写(或每次开短连接 + busy_timeout)。只在 register/login 时访问

TokenServiceHMAC 签名,无状态)

  • token 文本:base64url(payload) + "." + base64url(HMAC-SHA256(secret, payload))payload = "{pid}.{expUnixSeconds}"v1 固定前缀便于演进,记为 v1.{pid}.{exp})。
  • string Issue(int pid, TimeSpan ttl):默认 ttl 7 天(网关参数 --tokenTtlHours 可配)。
  • bool Verify(string token, out int pid):重算 HMAC 定长比较 + 校验未过期;任一不符返回 false。
  • 密钥:32 字节,来源优先级 --authSecretbase64> 环境变量 XWORLD_AUTH_SECRET > DB 同目录 auth.key 文件(不存在则生成随机 32 字节并以受限权限持久化)。密钥仅服务端,绝不下发客户端

网关 HTTP 端点(复用同一 Kestrel,经 Caddy 暴露在 game.xworld.link

  • GameServerHost 建 app 时映射(或抽到 AuthEndpoints.Map(app, store, tokens)):
    • POST /registerbody { "username", "password" }JSON)。校验非空/长度 → CreateAccount → 成功签 token 返回 { "token", "pid" };重复用户名 → 409 { "error": "username_taken" };非法输入 → 400。
    • POST /loginbody { "username", "password" }VerifyCredentials → 成功 { "token", "pid" };失败 → 401 { "error": "invalid_credentials" }
  • 端点不依赖 ServerLoop,纯同步/短异步,避免阻塞 tick。

网关 /ws 改造(GameServerHost.cs:49-77

  • 鉴权 opt-in(与现有 publicKeyPem==null 关签名同范式):StartAsync 新增 authSecret/authDbPath(或一个组装好的 TokenService/AccountStore)参数。
    • 配置了鉴权:/ws?token=TokenService.Verify 失败/缺失 → 401,不建连接;成功取 token 内 pid(忽略任何 ?pid=)。
    • 未配置鉴权(本地 dev / 现有测试):保留旧 ?pid= 路径,零回归。
  • pid 自此只来自 token(权威,DB 账号 id)。

Gateway.Runner 接线

  • 新增参数:--authDbPath <path>--authSecret <base64>(可选)、--tokenTtlHours <n>(默认 168)。
  • 生产由 install-services.ps1--authDbPath C:\xworld\accounts.db(密钥走 auth.key 自动生成或 --authSecret)。deploy/windows/install-services.ps1 与 README 同步更新。

AuthClient(新增,Client/Assets/Script/xmain/.../AuthClient.cs

  • 新增常量 GlobalData.ProductionAuthBase = "https://game.xworld.link"(与 WSS 同主机,HTTP 端点经 Caddy)。
  • IEnumerator Login(user, pwd, Action<bool ok, string token, int pid, string err>)Register(...)UnityWebRequest.PostGlobalData.ProductionAuthBase + "/login"(或 /register)。解析 JSON {token,pid}
  • 失败按状态码给出可读错误(409 用户名已占用 / 401 账号密码错 / 网络错)。

CSharpClientApp 改造

  • OnLoginClicked / OnRegisterClicked:改为调 AuthClient(协程),成功后 XData.SetString(LoginTokenKey, token)、记 pid、设 GlobalData.Token,再 EnterLobby;失败 ShowLoginError
  • ConnectLobby:用 wss://game.xworld.link/ws?token=<token>(替掉 WithPlayerId/?pid= 拼接)。token 从内存/XData 取。
  • 自动登录:启动时若 XData 有未过期 token(客户端可只存 + 直接试连,过期由服务端 401 反馈再回登录页),跳过手填。配合已有 remember/auto UI。
  • GetStablePlayerId 退役(pid 改由服务端下发)。

数据流(端到端)

  1. 注册/登录:客户端 HTTPS POST → 网关验密(PBKDF2 定长比较)→ TokenService.Issue(pid) → 返回 {token,pid}
  2. 连接:客户端 wss://…/ws?token=… → 网关 Verify → 取权威 pid → ConnectCommand
  3. 安全性:伪造 pid 不可能(无服务端密钥无法签出有效 token);冒充他人需其凭据或服务端密钥;明文 pid 路径在生产关闭。

错误处理

  • 注册:用户名空/过长/非法字符 → 400;重复 → 409;DB 写失败 → 500(日志含原因,不含密码)。
  • 登录:账号不存在或密码错 → 统一 401 invalid_credentials(不区分,防用户名枚举)。
  • WS:缺 token / 验签失败 / 过期 → 401,不建连接、不 Submit。
  • 客户端:网络失败/超时 → 提示重试;401 → 回登录页清本地 token。

测试(服务端 TDD,沿用 Server.Host.Tests/Gateway.Tests 套路,临时文件或内存 SQLite

  • AccountStore:建号成功返 id;重复用户名(含大小写 NOCASE)被拒;验密——对的过、错的拒、不存在的拒;密码不以明文存(读 BLOB 断言非明文);盐不同则相同密码哈希不同。
  • TokenServiceIssue→Verify 往返取回同 pid;篡改 payload/sig 任一被拒;过期被拒;换密钥被拒。
  • 端点(集成,真 Kestrel + HttpClient):register→拿 token→/ws 用该 token 连上(pid 与返回一致);重复 register 409;错密码 login 401。
  • WS:有效 token 建连且 pid 来自 token;缺/坏/过期 token → 401 且不建连接;未配鉴权时 ?pid= 仍可用(回归)。
  • 目标:dotnet test Server/Server.sln 全绿(现有用例不回归)。

安全默认与后续

  • 全程 HTTPS/WSSCaddy,已部署)。密钥 32B 仅服务端。密码 PBKDF2-SHA256+盐+≥10万迭代+定长比较。登录失败不泄露用户名是否存在。
  • 后续(本期不做):主动吊销/踢人(叠加 SQLite 会话表或短 token+刷新)、登录限流/锁定(Caddy 或网关计数)、找回密码、邮箱验证、token 刷新机制。

受影响文件清单(实施时)

  • 新增:Server/Auth/XWorld.Server.Auth.csprojAccountStore.csTokenService.csAuthEndpoints.cs)、Server/Auth.Tests/
  • 改:Server/Gateway/GameServerHost.cs/ws 验 token + 映射端点 + Start 参数)、Server/Gateway.Runner/Program.cs(参数接线)、Server/Server.sln(加工程)、deploy/windows/install-services.ps1 + README.md
  • 客户端:新增 AuthClient.cs;改 CSharpClientApp.cs(登录/注册/ConnectLobby/自动登录)、GlobalData.csProductionAuthBase)。