Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# 账号鉴权(真登录 + 签名 token)设计
|
||||
|
||||
日期:2026-06-24
|
||||
状态:已与用户确认设计,待写实施计划
|
||||
|
||||
## 背景与目标
|
||||
|
||||
当前网关 `/ws?pid=<int>` **直接信任客户端传来的明文 pid**(`Server/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.Auth`,net10.0,netstandard 不要求),仅在连接/登录时被调用,**不进 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
|
||||
```
|
||||
|
||||
### 组件与职责
|
||||
|
||||
**`AccountStore`(SQLite)**
|
||||
- 依赖 `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。
|
||||
- 密码哈希:`Rfc2898DeriveBytes`(PBKDF2-SHA256),每用户 16 字节随机盐,迭代数 ≥100000(存 `iter` 便于将来升级),输出 32 字节。比较用定长 `CryptographicOperations.FixedTimeEquals`。
|
||||
- 连接:单文件 `accounts.db`,路径由网关参数 `--authDbPath` 指定;开 WAL;`AccountStore` 内部串行化写(或每次开短连接 + `busy_timeout`)。**只在 register/login 时访问**。
|
||||
|
||||
**`TokenService`(HMAC 签名,无状态)**
|
||||
- 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 字节,来源优先级 `--authSecret`(base64)> 环境变量 `XWORLD_AUTH_SECRET` > DB 同目录 `auth.key` 文件(不存在则生成随机 32 字节并以受限权限持久化)。**密钥仅服务端,绝不下发客户端**。
|
||||
|
||||
**网关 HTTP 端点(复用同一 Kestrel,经 Caddy 暴露在 `game.xworld.link`)**
|
||||
- 在 `GameServerHost` 建 app 时映射(或抽到 `AuthEndpoints.Map(app, store, tokens)`):
|
||||
- `POST /register`:body `{ "username", "password" }`(JSON)。校验非空/长度 → `CreateAccount` → 成功签 token 返回 `{ "token", "pid" }`;重复用户名 → 409 `{ "error": "username_taken" }`;非法输入 → 400。
|
||||
- `POST /login`:body `{ "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 同步更新。
|
||||
|
||||
### 客户端(XWorld.Link 热更层)
|
||||
|
||||
**`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.Post` 到 `GlobalData.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 断言非明文);盐不同则相同密码哈希不同。
|
||||
- `TokenService`:Issue→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/WSS(Caddy,已部署)。密钥 32B 仅服务端。密码 PBKDF2-SHA256+盐+≥10万迭代+定长比较。登录失败不泄露用户名是否存在。
|
||||
- **后续(本期不做)**:主动吊销/踢人(叠加 SQLite 会话表或短 token+刷新)、登录限流/锁定(Caddy 或网关计数)、找回密码、邮箱验证、token 刷新机制。
|
||||
|
||||
## 受影响文件清单(实施时)
|
||||
|
||||
- 新增:`Server/Auth/`(`XWorld.Server.Auth.csproj`、`AccountStore.cs`、`TokenService.cs`、`AuthEndpoints.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.cs`(`ProductionAuthBase`)。
|
||||
Reference in New Issue
Block a user