Appearance
协议与外部集成
本文引用的文件
- service.go
- http.go
- fallback.go
- models.go
- ttl_test.go
- inbound.go
- outbound.go
- signer.go
- membernames.go
- provider.go
- userapi.go
- mailer.go
- retry.go
- mail.go
- email_verify.go
- outbound.go
目录
引言
本文面向需要对接 Yggdrasil、MUA Union、OIDC、邮件服务以及出站请求封装的运维与开发者。文档重点说明:
- Yggdrasil 协议实现如何兼容 authlib-injector 和 Mojang 官方端点;
- MUA Union 成员站的三头验签、成员站名解析、安全级别徽章、跨站绑定、同步与出站请求;
- OIDC 作为第三方登录提供者的角色,以及与外部身份提供方(企业微信、飞书等)的对接方式;
- 邮件服务的 SMTP 发送、重试、失败审计与模板国际化;
- outbound 出站封装的安全限制;
- 各协议的 curl 或伪客户端示例,以及联调排错清单。
项目结构
本仓库后端以 Go 包组织在 server/internal 下,按能力划分模块:
ygg:Yggdrasil 认证与会话服务器接口,包含元数据、鉴权、会话校验、Mojang fallback、TTL 窗口、签名与模型组装;union:Union 成员站模块,负责 Hub 到 Member 的入站签名路由、Member 到 Hub 的出站 API、纹理签名密钥轮换、成员站名缓存;oidc:基于 zitadel/oidc 实现的 OpenID Connect 提供者,暴露 discovery、userinfo、device flow、token 撤销等;mailer:基于标准库 SMTP 的通知发送器,含单次预算、可选重试、提交后不可重试语义;webi18n:邮件模板与多语言文案;store:数据库层,包含邮箱验证令牌、邀请码、玩家、OIDC 等表操作;outbound:通用受控出站 GET 客户端,用于皮肤市场、主题市场和成员站名解析等场景。
图表来源
章节来源
核心组件
- Yggdrasil 协议层:提供
/api/yggdrasil/{authserver,sessionserver,api}系列端点,返回 legacy 兼容 JSON,支持 metadata、authenticate、refresh、validate、invalidate、signout、join、hasJoined、profile、bulk profile lookup。 - Mojang Fallback:本地 miss 时尝试 Mojang sessionserver,但严格限制目标、重定向、地址族、响应大小与 JSON 合理性。
- TTL 双窗口:对 token 执行“软窗口”和“硬窗口”,validate/join 在软过期后拒绝,refresh 仍可在硬窗口内续期。
- Union 入站:所有 hub→member 请求经签名中间件校验,再路由至更新列表、更新私钥、同步、UUID remap、诊断、邮箱查询等。
- Union 出站:带 member key 的请求访问 Hub,涵盖 serverlist、privatekey、sync、bind、security level、OAuth2 backend key、黑名单代理等。
- UnionSigner:可热切换 hub 下发的纹理签名密钥,保留 local fallback,并具备自动轮换、异常暂停、每日变更预算审计。
- 成员站名解析:从每个成员的
/api/yggdrasil读取meta.serverName,带 TTL、失败窗口、并发 worker、Sweep 清理。 - OIDC 提供者:提供 discovery、userinfo、device flow、token 管理,注入共享 client、scope 与 claim 契约。
- 邮件服务:纯标准库 SMTP,支持隐式 TLS、STARTTLS、PLAIN 认证、预算窗口、一次回退重试、提交后禁止重复投递。
- 受限出站客户端:仅允许 HTTPS/HTTP、指定端口、白名单主机、无 userinfo、无重定向、响应大小上限、公共 IP 拨号。
章节来源
- http.go:56-103
- fallback.go:17-38
- ttl_test.go:12-27
- inbound.go:22-44
- outbound.go:20-40
- signer.go:24-46
- membernames.go:14-38
- provider.go:23-39
- mailer.go:19-38
- outbound.go:12-23
架构总览
下图展示客户端与外部系统之间的主要交互路径:
图表来源
详细组件分析
Yggdrasil 协议与 Mojang Fallback
Yggdrasil API 将 legacy 行为冻结为稳定契约:错误体统一为 {error, errorMessage, cause?},缺失资源使用 204,非 JSON POST 返回 200 + 错误体,而不是直接 4xx。Authenticate 与 Refresh 还叠加了 IP 滑动窗口限流与同用户名最小间隔保护。
Mojang Fallback 是 A2 最小兼容集:当本地没有对应 profile 或 join 信息时,才向 Mojang sessionserver 发起请求。其防护包括:
- BaseURL 固定,仅允许相同 scheme/host;
- UUID 必须为 32 位小写十六进制;
- 用户名必须符合 Mojang 字符集;
- 不跟随重定向;
- 只拨号公网 IP,拒绝环回、RFC1918、CGNAT、未指定地址;
- 响应最大 1 MiB;
- 响应必须能反序列化为带
id和name的 JSON 对象。
图表来源
协议示例
- 获取元数据:
curl -s https://your-site.example.com/api/yggdrasil | jq . - 登录:
curl -X POST https://your-site.example.com/api/yggdrasil/authserver/authenticate \ -H "Content-Type: application/json" \ -d '{"username":"Steve","password":"secret","clientToken":"ct","requestUser":true}' - 刷新:
curl -X POST https://your-site.example.com/api/yggdrasil/authserver/refresh \ -H "Content-Type: application/json" \ -d '{"accessToken":"at","clientToken":"ct","requestUser":false}' - 校验:
curl -v -X POST https://your-site.example.com/api/yggdrasil/authserver/validate \ -H "Content-Type: application/json" \ -d '{"accessToken":"at","clientToken":"ct"}' - 加入服务器:
curl -X POST https://your-site.example.com/api/yggdrasil/authserver/session/minecraft/join \ -H "Content-Type: application/json" \ -d '{"accessToken":"at","selectedProfile":{"id":"uuid"},"serverId":"sid"}' - 查询 profile:
curl -s https://your-site.example.com/api/yggdrasil/sessionserver/session/minecraft/profile/{uuid} - 批量查询:
curl -X POST https://your-site.example.com/api/yggdrasil/api/profiles/minecraft \ -H "Content-Type: application/json" \ -d '["Steve","Alex"]'
章节来源
TTL 缓存与诊断
Ygg 的 Token 双 TTL 策略遵循上游 Passport 语义:
- 小于软窗口:validate/join/refresh 均通过;
- 软窗口到硬窗口之间:validate/join 拒绝,refresh 仍可签发新 token;
- 超过硬窗口:两者都拒绝;
- 被标记 invalidated 的 token 始终失效;
- CreatedAt 为零且已软过期的 token 无法恢复;
- 改名 squeeze 会把 expires_at 压缩到改名时刻,使 validate 失败但 refresh 仍有恢复窗口。
诊断方面,Ygg API 通过 accesslog 记录 authenticate、refresh、validate、join、hasJoined、profile 等请求,支持最佳努力 capture,只在超级管理员诊断窗口打开时携带敏感详情。
图表来源
章节来源
纹理与 PNG 解析、UUID/PNG 生命周期
Ygg 的 textures property 由 AssembleTextures 构造:
- 输出
textures字段永远不是 null,空地图会序列化为{}; - SKIN 可附带
metadata.model,区分 default 与 slim; - CAPE 单独条目;
- value 是 base64 编码的 JSON;
- hasJoined 和 unsigned=false 请求会附加 RSA-SHA1 签名;
- uploadableTextures 值镜像 legacy 行为;
- UUID 在模型中保持 32 位小写十六进制,不含连字符。
PNG 解析本身不在该文件中展开,但 textures 的 value 通常承载 skin.png 与 texture.json 的 base64 数据;服务端会在 account/texture 管线中完成解码、去重与存储,而 Ygg 层只做序列化与签名。
章节来源
MUA Union 成员站模块
三头验签入站
Union 入站 API 将所有 hub→member 请求交给签名中间件校验。受保护路由包括 updateList、updatePrivateKey、updateBackendKey、sync、remapUUID、diagnose、queryEmail 和预留的 updatePlugin。自检描述路由 /api/union/member 及其无斜杠形式公开可用,供跨站浏览器读取。
图表来源
章节来源
成员站名解析
Hub 的 serverlist 中 name 可能为空,因此成员显示名称由本端后台向每个成员的 /api/yggdrasil 拉取。该过程:
- 只请求固定路径
/api/yggdrasil,不会拼接用户输入的路径; - 通过受限出站客户端访问,要求公网 IP、白名单主机、端口 80/443、无重定向、响应大小限制;
- 结果按 host 缓存,默认 TTL 24 小时,失败结果缓存 30 分钟;
- 并发 worker 默认 6,整体 Sweep 超时默认 90 秒;
- 成员名称最多 120 个 rune,防止长文本污染页面;
- 如果关闭功能,已学到的名称也不再继续服务。
图表来源
章节来源
安全级别徽章
安全级别由两步决定:
- 用当前 member key 换取短 code;
- 用 code 查询
/backend/{code}/security/level。
解析器同时容忍 hub 返回数字或字符串形式的级别,但不能解析或为空时会报错,而不是降级为 0。零级代表“未在联盟黑名单上”,并非“未知”。
章节来源
跨站绑定
跨站绑定流程:
- 发起方调用
/profile/bind获得 2 分钟有效 token; - 接收方调用
/profile/bindto完成绑定; - 支持
/profile/unbind解绑; - 支持
/profile/remapuuid跨联盟 UUID 重映射; - 支持
/profile/unmapped/byname/{name}列出名称冲突候选; - 支持
/profile/detail/{uuid}查看绑定详情; - 部分字段保持 json.RawMessage,避免 hub 侧字段漂移破坏解析。
图表来源
章节来源
同步与出站请求
成员站需要把本地玩家清单同步给 Hub,并在玩家创建、改名、删除时上报事件。Outbound 还提供 serverlist、privatekey、host public key、diagnose、OAuth2 backend key、黑名单代理等能力。所有出站请求都带 X-Union-Member-Key,并由 doJSON/roundTrip 统一封装,带超时、日志与错误包装。
章节来源
纹理签名密钥轮换
UnionSigner 支持三种模式:
- disabled:完全禁用;
- local:使用本地 fallback 签名;
- union-managed:使用 Hub 下发的私钥签名。
轮换策略:
- 每 10 到 15 分钟加抖动轮询
/privatekey; - 指纹变化才切换;
- 每天超过 3 次变更自动暂停轮换;
- 可通过面板恢复轮换;
- 支持手动 SetKey、RestoreKey、ForceRotation;
- 每次切换审计版本与指纹;
- 持久化失败也审计,但不阻止密钥生效。
图表来源
章节来源
OIDC 作为第三方登录提供者
OIDC Provider 基于 zitadel/oidc 实现,职责包括:
- 发布 discovery 文档,注入 shared_client_id、scopes_supported、claims_supported;
- 暴露 authorize、token、introspect、revoke、end_session、keys、device_authorization;
- 提供 /userinfo 与 yggc/userinfo;
- 支持 device flow,配置设备表单路径、有效期与轮询间隔;
- 维护客户端、授权记录、access_token、refresh_token;
- 提供 /api/v1/oidc/tokens 查询与撤销界面。
与传统 OAuth2/OIDC 对接企业微信、飞书等外部身份提供方时,通常由外部 IdP 充当 OIDC 上游,本系统作为 OP 暴露给 launcher 或客户端。若外部 IdP 不直接支持 OIDC,则需要在更上层做协议适配;本仓库中的 oidc 模块更适合作为本系统的身份中心,再由外部 IdP 与本系统对接。
图表来源
章节来源
邮件通知、邀请码与邮箱验证
邮件服务通过 SMTP 发送三类信:
- 密码重置;
- 邮箱验证;
- 管理面板测试邮件。
发送流程:
- 根据 SiteName 生成 subject;
- 使用 webi18n 生成多语言 text/html 内容;
- 选择隐式 TLS 或 STARTTLS;
- 使用 PLAIN 认证;
- 设置 dial window 与 session budget;
- 第一次失败后,若仍在预算内且错误可重试,等待 500ms 回退重试一次;
- 一旦 server 接受 DATA,不再重试,防止重复投递;
- 失败分类区分 DNS 临时错误、网络超时、连接中断、灰色列表等。
邮箱验证令牌:
- 只存 digest,不存明文;
- 单用户同一时刻只有一个未使用令牌;
- 过期与已使用行定期清理;
- Consume 原子更新 used_at,并返回 uid;
- 未知、过期、已使用统一返回 NotFound,避免状态预言机。
图表来源
章节来源
出站请求封装 outbound 的安全限制
受限出站客户端主要用于主题市场、皮肤市场和成员站名解析等受控下载场景,核心限制如下:
- 方法仅限 GET;
- 仅允许 http 与 https;
- 不允许 userinfo;
- 仅允许 80 与 443;
- 主机必须在白名单中,支持通配子域;
- 不跟随重定向;
- 响应长度先检查 Content-Length,再用 LimitReader 读一个字节验证;
- 拨号前解析全部 IP,只允许全局单播地址,拒绝环回、链路本地、私有网段、CGNAT、6to4、Teredo;
- 关闭 keep-alive,设置 TLS 握手与响应头超时。
图表来源
章节来源
依赖关系分析
Ygg 层依赖存储抽象、限流器、verifygate 与插件钩子;Union 入站依赖签名者、Outbound、Grantor、Hooks 与审计;UnionSigner 依赖 Outbound 与 ygg.Signer;OIDC Provider 依赖 zitadel/oidc、存储后端与审计;Mailer 依赖标准库 net/smtp;Outbound 受限客户端独立于其他协议,被多个子系统复用。
图表来源
- service.go:24-74
- http.go:56-103
- inbound.go:22-44
- outbound.go:20-40
- signer.go:24-46
- provider.go:23-39
- mailer.go:19-38
- outbound.go:12-23
章节来源
- service.go:24-74
- http.go:56-103
- inbound.go:22-44
- outbound.go:20-40
- signer.go:24-46
- provider.go:23-39
- mailer.go:19-38
- outbound.go:12-23
性能与容量特性
- Ygg 认证接口建议开启 IP 滑动窗口与同用户名 gap throttle,避免暴力破解;search profile 默认上限 5,可配置。
- Join Session TTL 为 120 秒,符合 legacy 部署。
- Mojang Fallback 单次请求超时 4 秒,响应最大 1 MiB,空闲连接池每主机 4,空闲超时 30 秒。
- UnionSigner 轮换周期 10-15 分钟加抖动,日变更超 3 次自动暂停。
- 成员站名解析默认 6 个 worker,TTL 24 小时,失败 TTL 30 分钟,Sweep 90 秒。
- Mailer 默认 dial 10 秒、session 30 秒,可配置 Budget;失败最多回退一次。
- 受限出站客户端默认超时 4 秒、最大响应 1 MiB,关闭 keep-alive。
[本节为通用指导,不直接分析具体代码片段]
联调与排错清单
Yggdrasil 联调
- 确认
/api/yggdrasil元数据返回signaturePublickey与skinDomains; - 检查 authenticate 是否返回 ForbiddenOperationException 或 TooManyRequests;
- 检查 refresh 是否接受
{id}与 bare UUID 两种 selectedProfile 形式; - 检查 validate 与 invalidate 是否返回 204;
- 检查 join 是否因邮箱未验证或 player mismatch 返回 ForbiddenOperationException;
- 检查 profile 是否返回 204、本地 JSON 或 Mojang Fallback 透传;
- 开启 accesslog 诊断窗口,观察 authenticate/refresh/validate/join/profile 的 Outcome、Who、Detail;
- 若启用 Mojang Fallback,确认开关、BaseURL、UUID 格式、用户名字符集、公网 IP 拨号、1 MiB 限制与 JSON 合理性。
Union 成员站联调
- 确认
/api/union/member自检文档可被浏览器跨域读取; - 确认 updateList、updatePrivateKey、updateBackendKey、sync、remapuuid、diagnose、queryemail 均经过签名中间件;
- 确认 privatekey 轮换后,Texture URL 签名使用新密钥,旧签名仍然有效;
- 确认 daily rotation 超过 3 次后自动暂停,需要人工 ResumeRotation;
- 确认 SecurityLevelNow 先换 code 再查 level,不能把 hub 不可达误判为 0 级;
- 确认 bind/bindto/unbind/remapuuid/unmapped/detail 路径参数正确转义;
- 使用 diagnose 端点检查 hub 连通性、TLS、时钟差异与原因分类。
OIDC 联调
- 确认
/.well-known/openid-configuration返回 shared_client_id、RS256、scopes 与 claims; - 确认 authorize、token、userinfo、revoke、end_session、keys、device_authorization 均可访问;
- 确认 device flow 的用户码、轮询间隔、表单路径与过期时间符合预期;
- 使用
/api/v1/oidc/tokens查看授权应用,使用 revoke 撤销单个或全部; - 对外部 IdP 对接时,优先确认 IdP 是否原生支持 OIDC;若不支持,应先在外部做协议桥接。
邮件服务联调
- 确认 Host、Port、User、Pass、From、SiteName 配置完整;
- 确认 465 走隐式 TLS,其他端口走 STARTTLS;
- 确认 Subject 与正文使用当前语言;
- 确认失败时是否出现可重试错误;
- 确认服务器接受 DATA 后不会重复投递;
- 检查邮箱验证令牌是否只存 digest,是否单用户单活令牌,是否按时清理。
受限出站联调
- 确认只允许白名单主机;
- 确认不支持非 80/443 端口;
- 确认 userinfo 被拒绝;
- 确认重定向被拒绝;
- 确认响应大小超过 MaxBytes 被拒绝;
- 确认域名解析后所有 IP 都是全局单播。
章节来源
- http.go:56-103
- http.go:240-500
- fallback.go:78-200
- inbound.go:60-200
- outbound.go:480-620
- signer.go:200-444
- provider.go:140-200
- userapi.go:16-124
- mailer.go:60-200
- retry.go:1-63
- email_verify.go:12-79
- outbound.go:30-167
结论
Yggdrasil 实现以 frozen legacy contract 为核心,保证 authlib-injector 与原版 Minecraft 客户端都能接入;Mojang Fallback 仅在本地 miss 时谨慎探活,绝不改变本地命中行为。MUA Union 通过三头验签、受限出站、密钥轮换与安全级别查询,把成员站与 Hub 的连接控制在可观测、可审计、可恢复的范围。OIDC 在本系统中作为身份提供者,对外暴露标准化 discovery 与 token 能力;邮件服务以标准库 SMTP 实现最小依赖发送,配合 webi18n 模板与 store 令牌机制完成通知与验证。outbound 受限客户端则为皮肤、主题与成员站名解析提供统一的 SSRF 与体积防护。联调时应优先检查协议入口、签名与限流、TTL 与 Fallback、Hub 连通性与 SMTP 配置,再逐步定位下游存储与外部 IdP。