diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md new file mode 100644 index 0000000..7756385 --- /dev/null +++ b/AUTHENTICATION.md @@ -0,0 +1,269 @@ +# 服主配置指南 + +本文面向部署 `bd2server` 的服主,不面向普通玩家。 + +此文为极其诡异不说人话的 GPT 系列模型编写,所以如果你看不懂,那么请丢给 AI。 + +为避免混淆,本文统一使用以下名称: + +- `provider access token`、`provider ID token`:Discord/Google 在 OAuth/OIDC 流程中返回的凭据,仅由服务端临时使用。 +- `game access token`、`game refresh credential`:`bd2server` 自己签发、供玩家客户端访问本服使用的凭据。 + +除非明确写出 `provider`,下文提到的 access token 或 refresh credential 都指 `bd2server` 自己签发的 game credential。 + +客户端会受到配置结果影响,例如显示哪些按钮、登录能否成功,但客户端不保存或管理 `client_secret`、master key。 + +也就是说,即使你服务器上什么都有,那他们的客户端 `GET /auth/config` 只能看到: + +```json +{ + "mode": "oauth", + "providers": ["discord", "google"] +} +``` + +玩家点击第三方登录后,服务端会为本次登录生成一次性随机 `state`、PKCE verifier/challenge,以及 Google OIDC `nonce`。Provider callback 必须匹配对应的短期 login transaction;`state`、授权码和登录事务不能跨事务复用。服主不需要手工生成或配置这些值。 + +## 1. 准备公网地址 + +联机服务器需要一个长期稳定、玩家和 provider 都能访问的 HTTPS origin(当然,我们都自部署服务器了,公网地址肯定是有的),例如: + +```text +https://bd2.example.com +``` + +要求: + +- `public_url` 只能包含 scheme、主机和可选端口,不能带路径、查询参数、片段或用户信息。 +- **bd2server 自身要求**公网 `public_url` 使用 HTTPS;只有 `localhost`、`127.0.0.1` 或 `::1` 开发环境允许 HTTP。Provider 还可能施加额外的 redirect URI 限制,最终 URI 也必须通过 Discord/Google 后台的校验。 +- 反向代理或内网穿透必须把 `/auth/`、`/game/` 和 `/assets/` 全部转发到我们启动的 `bd2server`。 +- 不要让代理缓存 `/auth/` 响应。 +- 域名变化后,必须同时修改 `authentication.json` 和 provider 后台的 redirect URI,然后重启服务端,并告知玩家。 + +固定回调地址为: + +```text +Discord: /auth/discord/callback +Google: /auth/google/callback +``` + +例如: + +```text +https://bd2.example.com/auth/discord/callback +https://bd2.example.com/auth/google/callback +``` + +## 2. 创建 Discord OAuth 应用 + +1. 登录 。 +2. 选择 **New App**,为自己的服务器创建一个应用。 +3. 在应用的 **OAuth2** 页面复制 **Client ID**。 +4. 创建应用后立即安全保存 **Client Secret**。如果后台已不显示 secret,再使用 Discord 提供的重置/轮换功能生成新 secret,并同步更新服务端环境变量。不要为了“查看”而无意义地重置仍在使用的 secret。 +5. 在 **Redirects** 中添加: + + ```text + https://bd2.example.com/auth/discord/callback + ``` + +6. 保存设置。 + +若重置 Discord client secret,旧 secret 会立即失效;更新服务器环境变量并重启即可,玩家客户端不需要改变。Authorization Code 流程使用的一次性 `state` 和 PKCE 由 `bd2server` 自动生成并在 callback 时校验。 + +## 3. 创建 Google OAuth 应用 + +Google 当前主要在 **Google Auth Platform → Branding / Audience / Data Access / Clients** 中管理 OAuth。控制台名称以后可能调整,但需要完成的对象不变:配置应用展示与受众,并创建一个 **Web application** 类型的 OAuth client。 + +1. 登录 并选择或创建项目。 +2. 在 **Branding** 中配置应用名称、支持邮箱等公开信息。 +3. 在 **Audience** 中选择实际目标受众并确认 Publishing status: + - **测试阶段**:保持 Testing,并把实际参加登录测试的 Google 账号加入 Test users。未列入的玩家会被 Google 拒绝。 + - **向玩家开放前**:检查 Audience、Publishing status 和 Google 显示的发布要求,并按实际受众切换到 In production。 +4. 在 **Data Access** 中核对 scope。`bd2server` 当前只申请最小的 `openid`,不申请 `profile` 或邮箱 scope。 +5. 在 **Clients** 中选择 **Create Client**,应用类型选择 **Web application**。不要创建 Desktop application 类型的 client:浏览器回调落在服务端 HTTPS endpoint,需要 Web application client。 +6. 在 **Authorized redirect URIs** 中添加: + + ```text + https://bd2.example.com/auth/google/callback + ``` + +7. 创建后立即安全保存 Client ID 和 Client Secret。如果 Google 后台不再允许查看或下载原 secret,就按后台提供的轮换流程创建新 secret,并同步更新服务端环境变量。 + +## 4. 生成并保存 master key + +每台服务器生成一次 32-byte 随机 master key。 + +Windows(PowerShell 7): + +```powershell +$keyBytes = [byte[]]::new(32) +$rng = [Security.Cryptography.RandomNumberGenerator]::Create() +try { + $rng.GetBytes($keyBytes) + [Convert]::ToBase64String($keyBytes) +} finally { + $rng.Dispose() + [Array]::Clear($keyBytes, 0, $keyBytes.Length) +} +``` + +macOS/Linux(Bash 或 Zsh): + +```bash +master_key="$(openssl rand -base64 32)" +printf '%s\n' "$master_key" +unset master_key +``` + +把输出保存进密码管理器或服务部署系统的 secret store。不要提交到 Git,也不要写进 `authentication.json`。 + +master key 必须长期稳定: + +- 重启服务端时继续使用同一个值。 +- 备份 `auth.db` 时也要单独安全备份 master key。 +- master key 会派生两类用途隔离密钥:HMAC 密钥用于 provider identity、game token、登录事务 secret、OAuth `state` 和客户端地址的稳定索引;AES-256-GCM 密钥用于 PKCE verifier、OIDC `nonce` 和待领取登录结果。 +- 丢失或随意更换 master key 后,已有 HMAC 索引无法再匹配,已有 AES-GCM 密文也无法解密;现有 `auth.db` 认证状态不能继续使用。 +- 不要在两台互不信任的服务器之间共用 master key。 + +## 5. 编辑服务器配置 + +编辑与 `bd2server` 可执行文件同目录的 `authentication.json`。Discord 和 Google 都启用时: + +```json +{ + "mode": "oauth", + "public_url": "https://bd2.example.com", + "master_key_env": "BD2_AUTH_MASTER_KEY", + "providers": { + "discord": { + "client_id": "123456789012345678", + "client_secret_env": "BD2_DISCORD_CLIENT_SECRET" + }, + "google": { + "client_id": "example.apps.googleusercontent.com", + "client_secret_env": "BD2_GOOGLE_CLIENT_SECRET" + } + }, + "session": { + "access_ttl": "15m", + "refresh_ttl": "720h", + "device_transaction_ttl": "10m" + } +} +``` + +配置字段 `device_transaction_ttl` 以及服务端日志中可能出现的 login transaction,指 `bd2server` 自己用于“游戏客户端发起登录 → 浏览器完成 Authorization Code → 游戏客户端领取结果”的短期交接机制。它不是 Discord 或 Google 的 OAuth Device Authorization Grant。 + +只启用一个 provider 时,删除另一个 provider 对象即可。`providers` 是对象映射,不是字符串数组。 + +JSON 中可以保存 `client_id`,因为它不是秘密;只能保存 secret 所在环境变量的名称,不能保存 secret 实值。 + +## 6. 向服务端进程注入 secret + +临时测试时,在启动 `bd2server` 的同一个 shell 中设置。 + +Windows(PowerShell 7): + +```powershell +$env:BD2_AUTH_MASTER_KEY = '' +$env:BD2_DISCORD_CLIENT_SECRET = '' +$env:BD2_GOOGLE_CLIENT_SECRET = '' + +.\bd2server.exe serve +``` + +macOS/Linux(Bash 或 Zsh): + +```bash +export BD2_AUTH_MASTER_KEY='' +export BD2_DISCORD_CLIENT_SECRET='' +export BD2_GOOGLE_CLIENT_SECRET='' + +./bd2server serve +``` + +如果只启用 Discord,就不需要设置 Google secret;反之亦然。服务端在 OAuth 配置或任何必需环境变量缺失时会拒绝启动。 + +## 7. 启动后检查 + +先检查服务健康。 + +Windows(PowerShell 7): + +```powershell +$publicUrl = 'https://bd2.example.com' +(Invoke-WebRequest -Uri "$publicUrl/healthz").Content +``` + +macOS/Linux(Bash 或 Zsh): + +```bash +PUBLIC_URL='https://bd2.example.com' +curl --fail --silent --show-error "$PUBLIC_URL/healthz" +``` + +预期输出: + +```text +ok +``` + +再检查公开认证策略。 + +Windows(PowerShell 7): + +```powershell +Invoke-RestMethod -Uri "$publicUrl/auth/config" | ConvertTo-Json +``` + +macOS/Linux(Bash 或 Zsh): + +```bash +curl --fail --silent --show-error "$PUBLIC_URL/auth/config" +``` + +正常响应只应包含公开认证策略,例如 `mode` 和 provider 名称: + +```json +{ + "mode": "oauth", + "providers": ["discord", "google"] +} +``` + +最后再启动玩家客户端。首次进入应显示服主启用的 Discord/Google 按钮;浏览器地址应属于 Discord/Google,回调应返回配置的 `public_url`。 + +## 9. 常见问题 + +### 客户端需要填写 client ID 或 secret 吗? + +不需要。客户端只读取服务端公开的 provider 列表。Client ID 由服务端构造 OAuth 授权 URL 时使用;client secret 只参与服务端与 provider token endpoint 之间的通信。浏览器中的 provider 授权 URL 会包含 client ID,这是正常行为,因为 client ID 本来就不是 secret。 + +### client ID 写错会破坏玩家存档吗? + +这些配置错误通常不会修改 `state.db`,但可能导致认证失败、callback 无法完成、请求被送到错误 host,或 login transaction 超时。修正配置并重启服务端即可;客户端不会因此得到 client secret。 + +### 可以让所有服主共用项目作者的一组 client ID/secret 吗? + +不建议,也不应分发共享 client secret。每位服主应创建自己的 OAuth 应用,独立控制回调域名、配额、停用和审计。共享应用会把所有服务器的信任、限流和泄漏风险绑在一起。 + +### 换域名需要重新编译客户端插件吗? + +登录插件本身不需要重新编译,但客户端最初连接的服务端入口、`authentication.json` 的 `public_url`、反向代理以及 provider redirect URI 必须全部指向新地址并保持一致。 + +### 可以把服务直接暴露在公网 HTTP 上吗? + +不可以。公网 OAuth 会传输一次性 device secret、登录结果和游戏凭据,必须由 HTTPS 保护。只有同机 loopback 开发测试允许 HTTP。 + +### 如何撤销当前客户端自动登录? + +客户端退出账号时会删除当前服务器 origin 对应的安全 refresh credential。服主还可以调用服务端 session revoke 流程;不要通过删除 `state.db` 来“清登录”,玩家存档与认证数据是分开的。 + +### 如何重置服务器 owner? + +这属于破坏性管理操作。当前没有公开的 owner 重置 API,不要手工改 SQLite。应先停止服务、完整备份并校验 `state.db`、`auth.db` 和 master key,再使用未来提供的受控管理工具。删除 `state.db` 会删除玩家存档,不是正确的认证重置方法。 + +## 10. 官方教程 + +- Discord OAuth2: +- Google OAuth 2.0 for Web Server Applications: diff --git a/RESOURCES.md b/RESOURCES.md new file mode 100644 index 0000000..f5f2890 --- /dev/null +++ b/RESOURCES.md @@ -0,0 +1,162 @@ +# 服主资源与 CDN 配置指南 + +`bd2server` 不要求 Windows 游戏目录,也不要求玩家的本地 ServerData 目录。服务端只会自动准备自身逻辑需要的 GameData。玩家在 `bd2client` 中有三种资源来源: + +- **官方 CDN**:玩家直接从官方节点下载。 +- **服务器资源源**:客户端向游戏服务器查询资源 URL;服主可以在 URL 后部署自建镜像或官方 CDN 反代,两者使用同一种协议。 +- **本地资源目录**:客户端使用玩家已经下载的资源;目录选择和校验完全发生在玩家设备上。 + +无论怎样,最终的资源都是要来到玩家的客户端的。 + +`resources.json` 只配置 `official` 或 `server`。服务端不会接收、保存或返回玩家的本地资源路径。 + +## 朋友小服与 FRP + +推荐在 `resources.json` 中使用默认配置: + +```json +{ + "mode": "official" +} +``` + +玩家在 `bd2client.exe` 中选择“官方 CDN”。资源会由玩家直连官方节点,FRP 只承载认证和游戏 API,不承担数 GB 的 ServerData 流量。 + +## 获取官方资源镜像 + +服主准备自建 CDN 时,可以运行: + +Windows(PowerShell 7): + +```powershell +.\bd2server.exe resources fetch --output 'D:\bd2-resources' +``` + +macOS/Linux(Bash 或 Zsh): + +```bash +./bd2server resources fetch --output '/srv/bd2-resources' +``` + +命令会按照同目录 `versions.json` 中的版本: + +- 下载并验证 GameData、`design.version`、ZIP 结构与 CRC。 +- 下载 ServerData catalog,并只同步 catalog 实际引用的 bundle。 +- 拒绝不安全的 catalog 路径。 +- 跳过已经具有有效 `UnityFS` 签名的 bundle。 +- 使用同目录临时文件完成替换。 +- 生成 `resource-fetch-manifest.json`。 + +ServerData 可能达到数 GB。命令不会在普通 `serve` 启动时自动下载整套 ServerData,避免意外消耗大量磁盘和流量。 + +同步结果的目录结构是: + +```text +/ +├─ ServerData/StandaloneWindows64/HD//... +├─ GameData//... +└─ resource-fetch-manifest.json +``` + +## 服务器资源源 + +服主自建资源镜像与反代官方 CDN 对客户端来说没有区别:它们都提供两个可公开访问的 HTTP(S) 基础 URL。服务端统一使用 `server` 模式,不会向玩家暴露后端究竟是静态文件、对象存储、边缘 CDN 还是缓存反代。 + +把上述 `ServerData` 和 `GameData` 目录上传到对象存储、静态 Web 服务器或 CDN 时,资源 URL 必须保留目录层级,并允许普通 `GET` 请求。 + +例如: + +```json +{ + "mode": "server", + "server_data_url": "https://cdn.example.com/ServerData", + "game_data_url": "https://cdn.example.com/GameData" +} +``` + +保存为 `bd2server` 可执行文件同目录的 `resources.json`,然后重启服务端。玩家在 `bd2client.exe` 中选择“服务器资源源”;LI 插件会执行: + +```http +PUT /client/resources +Content-Type: application/json + +{"cdn_mode":"server"} +``` + +服务端会返回经过校验的 URL、Bundle 版本和 GameData 版本。客户端拒绝模式或版本不一致的响应。 + +### 反代官方 CDN + +如果服主不保存完整镜像,也可以把相同的 `server` 配置指向已有边缘 CDN 或带磁盘缓存的 Nginx。反代仍会消耗服主的回源/出口流量,不建议直接放在窄带 FRP 后面。 + +Nginx 参考配置: + +```nginx +proxy_cache_path /var/cache/nginx/bd2 levels=1:2 keys_zone=bd2:256m max_size=100g inactive=30d use_temp_path=off; + +server { + listen 443 ssl http2; + server_name cdn.example.com; + + location /ServerData/ { + proxy_pass https://bd2-cdn.akamaized.net/ServerData/; + proxy_set_header Host bd2-cdn.akamaized.net; + proxy_ssl_server_name on; + proxy_cache bd2; + proxy_cache_lock on; + } + + location /GameData/ { + proxy_pass https://bd2-cdn.akamaized.net/GameData/; + proxy_set_header Host bd2-cdn.akamaized.net; + proxy_ssl_server_name on; + proxy_cache bd2; + proxy_cache_lock on; + } +} +``` + +TLS 证书、缓存容量、访问日志和服务条款由服主负责。公网资源 URL 必须使用 HTTPS;只有 loopback 开发地址允许 HTTP。URL 不能包含用户凭据、查询参数、fragment 或尾部 `/`。 + +## 本地已下载资源 + +适用于玩家已经自行取得当前版本完整资源的情况。资源来源需要玩家自行解决。 + +选择的目录必须是同时包含 `ServerData` 和 `GameData` 的根目录,结构如下: + +```text +<本地资源根目录>/ +├─ ServerData/ +│ └─ StandaloneWindows64/ +│ └─ HD/ +│ └─ / +│ ├─ catalog_alpha.json +│ ├─ catalog_alpha.hash +│ └─ +└─ GameData/ + └─ / + ├─ design.version + └─ release/ + ├─ common-dbdata.info + └─ common-dbdata.bin +``` + +`` 和 `` 必须分别等于客户端包内 `versions.json` 的 `bundle_version` 与 `game_data_version`。例如当前发布版本对应: + +```text +ServerData/StandaloneWindows64/HD/20260921135230/ +GameData/20260923193640/ +``` + +准备完成后,在 `bd2client` 中选择“本地已下载资源”,并选中最外层的 `<本地资源根目录>`,不要直接选择 `ServerData`、`GameData` 或某个版本号目录。客户端会验证目录结构和版本,然后由 LI 插件在游戏进程内把该目录作为仅监听 loopback 的本机资源源提供给游戏。校验失败时需要玩家自行修正或重新准备资源,客户端不会跨版本拼接或自动从网络补齐缺失文件。 + +## 版本更新 + +每次更新 `versions.json` 后: + +1. 重新运行 `bd2server resources fetch`。 +2. 等待服务器资源源完成上传,或确认反代缓存已经准备好。 +3. 确认新版本目录可以访问。 +4. 再启动新版本服务端并让玩家更新客户端工具包和插件。 + +不要把新 catalog 与旧 bundle 目录混用,也不要只修改 `resources.json` 中的 URL 而忽略版本文件。 \ No newline at end of file diff --git a/build-release.ps1 b/build-release.ps1 index 9d4933d..11e5da9 100644 --- a/build-release.ps1 +++ b/build-release.ps1 @@ -97,8 +97,8 @@ Copy-Item -LiteralPath $authenticationConfig -Destination (Join-Path $serverPack Copy-Item -LiteralPath $resourceConfig -Destination (Join-Path $serverPackage 'resources.json') -Force Copy-Item -LiteralPath $versionConfig -Destination (Join-Path $clientPackage 'versions.json') -Force Copy-Item -LiteralPath (Join-Path $root 'RELEASE.md') -Destination (Join-Path $serverPackage 'README.md') -Force -Copy-Item -LiteralPath (Join-Path $root 'docs\AUTHENTICATION.md') -Destination (Join-Path $serverPackage 'AUTHENTICATION.md') -Force -Copy-Item -LiteralPath (Join-Path $root 'docs\RESOURCES.md') -Destination (Join-Path $serverPackage 'RESOURCES.md') -Force +Copy-Item -LiteralPath (Join-Path $root 'AUTHENTICATION.md') -Destination (Join-Path $serverPackage 'AUTHENTICATION.md') -Force +Copy-Item -LiteralPath (Join-Path $root 'RESOURCES.md') -Destination (Join-Path $serverPackage 'RESOURCES.md') -Force Copy-Item -LiteralPath (Join-Path $root 'docs\CLIENT.md') -Destination (Join-Path $clientPackage 'README.md') -Force Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination (Join-Path $serverPackage 'LICENSE') -Force Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination (Join-Path $clientPackage 'LICENSE') -Force