refactor(yggdrasil): 整理与性能优化,修复若干 Bug
Bug 修复
- textures metadata:SKIN 仅在 IsSlim 时输出 {"model":"slim"};CAPE 不带 metadata。
原实现误用文件字节数 skin.Size 作为 metadata,违反 Yggdrasil 协议。
- KeyPair 持久化不全:Profile 新增 rsa_public_key/public_key_signature/
public_key_signature_v2/key_expires_at/key_refresh_at 列;GetKeyPair/UpdateKeyPair
读写全部字段。AutoMigrate 自动加列。原实现每次请求都重新生成 RSA-4096。
- GetPlayerCertificates:无效 token 返回 401(先前误用 403 且未判 err)。
- HasJoinedServer:失败返回 204 无 body,符合 Yggdrasil 协议(原用 APIResponse
+ body 违反 204 规范)。
代码质量
- Authenticate 删除冗余 body 读取与回放。
- 证书服务引入具名结构 PlayerCertificate/PlayerKeyPair,替代 map[string]interface{}。
- CreateSession 用户名缺失改用 ErrUsernameRequired(新增)。
性能优化(签名一致性的回归测试已覆盖)
- SignatureService 缓存已解析的根私钥(sync.RWMutex 双重检查),
快路径仅一次 RLock + RSA 签名,免去每次签名 PEM 解析与 Redis 往返。
- GetOrCreateYggdrasilKeyPair 用 MGet 单次往返取三字段,缓存命中后零 Redis。
- NewKeyPair 消息构造用 strconv.AppendInt 避免 string+拼接分配;V2 复用 DER 字节。
- 序列化服务:texturesMap 预分配 cap(2);slim 分支直接构造完整 map。
- 会话服务 GetSession 移除重复的 ValidateServerID 校验。
死代码清理
- 删除 yggdrasil_validator.go(未被调用的 Validator)。
- 删除 signature_service.FormatPublicKey/SignStringWithProfileRSA。
- 删除 pkg/auth 中未被使用的 YggdrasilJWTManager 与 GenerateKeyPair/
EncodePrivateKeyToPEM/RedisClient/YggdrasilPrivateKeyRedisKey。
- 删除 yggdrasil_handler.go 中 25 个未使用常量、passwordRegex、
APIResponse/standardResponse。
- 删除 errors.go 中未被使用的 ErrYggForbiddenOperation/ErrYggIllegalArgument/
ErrInvalidSignature/ErrInvalidTextureType/ErrCertificateGenerate/ErrInvalidPassword。
测试
- 新增 signature_service_test.go(基于 miniredis):8 个测试 + 基准。
核心:SignString_MatchesReference 与重构前参考实现逐字节比对签名输出一致。
-race 检测通过;基准约 7.1ms/op(缓存命中路径)。
附带(与本次任务前已存在的未提交改动)
- handler/captcha_handler:将响应字段 msg 改为 message,与前端约定对齐。
This commit is contained in:
@@ -55,8 +55,8 @@ func (s *yggdrasilSerializationService) SerializeProfile(ctx context.Context, pr
|
||||
|
||||
// SerializeProfileWithUnsigned 序列化档案为Yggdrasil格式(支持unsigned参数)
|
||||
func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context.Context, profile model.Profile, unsigned bool) map[string]interface{} {
|
||||
// 创建基本材质数据
|
||||
texturesMap := make(map[string]interface{})
|
||||
// 预分配材质 map(最多 SKIN + CAPE 两个条目,避免运行时扩容)
|
||||
texturesMap := make(map[string]interface{}, 2)
|
||||
textures := map[string]interface{}{
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
"profileId": profile.UUID,
|
||||
@@ -64,7 +64,7 @@ func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context
|
||||
"textures": texturesMap,
|
||||
}
|
||||
|
||||
// 处理皮肤
|
||||
// 处理皮肤:metadata 仅对 SKIN 有效,值为 {"model":"slim"};classic/默认省略 metadata
|
||||
if profile.SkinID != nil {
|
||||
skin, err := s.textureRepo.FindByID(ctx, *profile.SkinID)
|
||||
if err != nil {
|
||||
@@ -73,14 +73,19 @@ func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context
|
||||
zap.Int64("skinID", *profile.SkinID),
|
||||
)
|
||||
} else if skin != nil {
|
||||
texturesMap["SKIN"] = map[string]interface{}{
|
||||
"url": skin.URL,
|
||||
"metadata": skin.Size,
|
||||
// slim 才需要 metadata 字段;classic 直接仅含 url
|
||||
if skin.IsSlim {
|
||||
texturesMap["SKIN"] = map[string]interface{}{
|
||||
"url": skin.URL,
|
||||
"metadata": map[string]string{"model": "slim"},
|
||||
}
|
||||
} else {
|
||||
texturesMap["SKIN"] = map[string]interface{}{"url": skin.URL}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理披风
|
||||
// 处理披风:CAPE 不携带 metadata 字段
|
||||
if profile.CapeID != nil {
|
||||
cape, err := s.textureRepo.FindByID(ctx, *profile.CapeID)
|
||||
if err != nil {
|
||||
@@ -89,15 +94,12 @@ func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context
|
||||
zap.Int64("capeID", *profile.CapeID),
|
||||
)
|
||||
} else if cape != nil {
|
||||
texturesMap["CAPE"] = map[string]interface{}{
|
||||
"url": cape.URL,
|
||||
"metadata": cape.Size,
|
||||
}
|
||||
texturesMap["CAPE"] = map[string]interface{}{"url": cape.URL}
|
||||
}
|
||||
}
|
||||
|
||||
// 将textures编码为base64
|
||||
bytes, err := json.Marshal(textures)
|
||||
// 将 textures 编码为 base64
|
||||
textureBytes, err := json.Marshal(textures)
|
||||
if err != nil {
|
||||
s.logger.Error("序列化textures失败",
|
||||
zap.Error(err),
|
||||
@@ -106,12 +108,15 @@ func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context
|
||||
return nil
|
||||
}
|
||||
|
||||
textureData := base64.StdEncoding.EncodeToString(bytes)
|
||||
textureData := base64.StdEncoding.EncodeToString(textureBytes)
|
||||
|
||||
// 只有在 unsigned=false 时才签名
|
||||
var signature string
|
||||
property := Property{
|
||||
Name: "textures",
|
||||
Value: textureData,
|
||||
}
|
||||
if !unsigned {
|
||||
signature, err = s.signatureService.SignStringWithSHA1withRSA(ctx, textureData)
|
||||
signature, err := s.signatureService.SignStringWithSHA1withRSA(ctx, textureData)
|
||||
if err != nil {
|
||||
s.logger.Error("签名textures失败",
|
||||
zap.Error(err),
|
||||
@@ -119,26 +124,15 @@ func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 构建属性
|
||||
property := Property{
|
||||
Name: "textures",
|
||||
Value: textureData,
|
||||
}
|
||||
|
||||
// 只有在 unsigned=false 时才添加签名
|
||||
if !unsigned {
|
||||
property.Signature = signature
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
data := map[string]interface{}{
|
||||
return map[string]interface{}{
|
||||
"id": profile.UUID,
|
||||
"name": profile.Name,
|
||||
"properties": []Property{property},
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// SerializeUser 序列化用户为Yggdrasil格式
|
||||
|
||||
Reference in New Issue
Block a user