RhythMC-Reborn 登录认证流程文档
目录
认证概述
双层认证模型
RhythMC-Reborn 采用双层认证模型,确保系统安全性:
认证模式对比
| 模式 | 用户类型 | 凭证 | 安全级别 |
|---|---|---|---|
| ONLINE | 正版用户 | Mojang UUID + 可选令牌 | 高 |
| TOKEN | 所有用户 | Net Token | 中 |
| OFFLINE_REFRESH | 已开启续关的玩家 | 本地缓存 resumeToken | 中低 |
服务器认证流程
完整流程图
服务器登录请求当前包含 pluginVersion、pluginSHA,并通过 Authorization: Bearer <serverBootstrapToken> 建立服务器身份;成功响应返回服务器基础信息与短期 serverAccessToken。登录成功后,后续受保护 HTTP 请求和 WebSocket 握手统一使用 Authorization: Bearer <serverAccessToken>。
当前实现中,ResourcePackManager 本身在 Main 中先于 NetworkManager 构造,但真正的远程资源包列表请求会延后到服务器登录成功后的初始化阶段,由 NetworkManager 串到同一条 CompletableFuture 链中。这样可以确保资源包接口使用已经完成认证的 HttpClient;若登录第一步或后续初始化阶段失败,则会立即注册本地资源包 fallback,避免离线模式下资源包索引为空。
serverAccessToken 当前有效期默认 10 分钟。客户端在 WebSocket 非预期断开后会先调用 GET /server/refresh 刷新令牌;若刷新失败,则立即使旧令牌失效,并重新执行完整的服务器登录流程。
同一 serverID 若在旧会话尚未显式 /server/logout 的情况下重新执行 /server/login,后端会先强制踢掉旧的服务器会话,再签发新的 serverAccessToken。旧会话下挂着的玩家在线 session 也会被一起清理,避免 Redis 中残留并发的活动 serverSession。
服务器登录令牌配置
玩家认证流程
三种认证模式
当 NetworkManager 处于离线模式时,PlayerManager.beginLogin() 会跳过所有 /player/login 请求,直接注册默认 RhyPlayerProfileJson,并向玩家显示“游客模式 / 网络不可用”的 Title 与提示消息。该模式不会写入 TokenStore,也不会创建远端玩家会话。
OFFLINE_REFRESH 现在默认关闭。后端仅在玩家选项 options.continueEnabled = true 时下发 resumeToken;默认值为 false。客户端只有在本地 TokenStore 持有有效 resumeToken 时才会尝试自动续登。
ONLINE 模式 (正版自动登录)
流程:
请求示例:
{
"mode": "ONLINE",
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"version": 1,
"authToken": "optional-token"
}
TOKEN 模式 (手动令牌登录)
流程:
请求示例:
{
"mode": "TOKEN",
"version": 1,
"authToken": "user-provided-token-string"
}
OFFLINE_REFRESH 模式 (已开启续关时的自动续登)
流程:
请求示例:
{
"mode": "OFFLINE_REFRESH",
"version": 1,
"resumeToken": "in-memory-cached-token"
}
当前实现把 resumeToken 保存到插件本地的临时缓存文件 continue-tokens.properties。它仍然不是数据库持久化,也不会上传到后端;插件重载后可继续用于 OFFLINE_REFRESH 自动续登,但整服/JVM 关闭时会被清空。
仅当玩家显式开启 options.continueEnabled 时,/player/login 成功响应才会包含 resumeToken。如果玩家关闭该选项,后端会停止下发新 resumeToken 并撤销已有缓存 token;插件在后续登录或选项同步成功后也会清理本地 TokenStore。
插件重载后,Main.onEnable() 会重新遍历当前仍在线的 Bukkit 玩家并重新执行 beginLogin()。因此只要本地 resumeToken 还有效,已在线玩家无需重新进服也能恢复远端登录状态。
玩家登录时还会把当前客户端玩家数据版本一起上传。后端会在 players.profile_version 中保存新的版本值,但本次 /player/login 响应中的 version 字段仍回显登录前保存的版本,供插件完成兼容性判断:
version > plugin-version-code: 视为玩家数据比当前插件新,立即踢出,避免旧插件错误解析新数据。version < plugin-version-code: 允许登录,但提示玩家当前档案较旧,必要时联系管理员执行数据升级。- 登录成功后,后端把该玩家的已保存版本更新为本次请求提交的版本,后续再次登录不再重复提示同一版本差异。
玩家退出服务器时,客户端会额外发送一次 POST /player/logout,并携带 accessToken 与 uid,用于通知服务端结束玩家在线会话;该请求为 best-effort,不影响本地退出流程。
如果 POST /player/login 返回 409,表示该账号当前仍有活动玩家会话。插件不会把这类响应计成普通 Token 错误,而是继续保留手动登录状态,并向玩家显示“当前帐号已登录,如已登出请等一分钟后再试。”。
为避免玩家长时间停留在大厅、菜单或未发生上传请求时短期 accessToken 因空闲而过期,插件现在会按 config.yml -> net.player-session-refresh-interval-seconds 周期性重发一次玩家状态同步。该同步会复用现有 resume_session / 玩家状态上传链路,从而刷新后端里的玩家 session token TTL;默认间隔为 240 秒。
Mojang 验证集成
验证流程
UUID 格式化
// 输入: 无连字符的 UUID
// "550e8400e29b41d4a716446655440000"
// 输出: 标准格式的 UUID
// "550e8400-e29b-41d4-a716-446655440000"
public static String formatUuid(String rawUuid) {
return rawUuid.replaceFirst(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5"
);
}
安全风险
当前实现的安全问题:
// TODO 注释: 有安全风险,需要修复
public static CompletableFuture<Boolean> verifyOnline(String username, String uuid) {
// 问题: 任何人知道正版用户名都可以伪造其身份
// 当前只验证用户名对应的 UUID 是否匹配
// 未验证玩家是否真正拥有该账号
}
建议修复方案:
会话管理
ServerSession 结构
public class ServerSession {
private final String sessionID; // 本地生成,不可变
private int serverID; // 登录后由服务器分配
private String serverName; // 服务器名称
private String assetServer; // 资源服务器地址
private String accessToken; // 服务端短期访问令牌
private String serverIP; // 服务器公网IP
private String country; // 所在国家/地区
private String wsServer; // WebSocket服务器地址
private String pluginVersion; // 插件版本
private NetworkStatus status; // 当前网络状态
}
会话生命周期
会话重置 (重连)
安全考量
安全措施清单
| 措施 | 实现状态 | 描述 |
|---|---|---|
| 服务器登录凭证 | ✅ 已实现 | /server/login 从请求头读取 bootstrap bearer |
| TLSv1.3 | ✅ 已实现 | 最新 TLS 协议 |
| ECC 加密 | ✅ 已实现 | 椭圆曲线加密算法 |
| 强密码套件 | ✅ 已实现 | 仅允许安全密码套件 |
| 会话隔离 | ✅ 已实现 | 每次连接生成新会话ID |
| Mojang 验证 | ⚠️ 需改进 | 存在身份伪造风险 |
| 本地令牌缓存 | ✅ 已实现 | TokenStore 仅缓存已开启续关玩家的 resumeToken,可跨插件重载恢复,整服关闭后清空 |