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,与前端约定对齐。
128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
apperrors "carrotskin/internal/errors"
|
|
"carrotskin/internal/repository"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// CertificateService 证书服务接口
|
|
type CertificateService interface {
|
|
// GeneratePlayerCertificate 生成玩家证书
|
|
GeneratePlayerCertificate(ctx context.Context, uuid string) (*PlayerCertificate, error)
|
|
// GetPublicKey 获取公钥
|
|
GetPublicKey(ctx context.Context) (string, error)
|
|
}
|
|
|
|
// PlayerCertificate 玩家证书
|
|
type PlayerCertificate struct {
|
|
KeyPair PlayerKeyPair `json:"keyPair"`
|
|
PublicKeySignature string `json:"publicKeySignature"`
|
|
PublicKeySignatureV2 string `json:"publicKeySignatureV2"`
|
|
ExpiresAt int64 `json:"expiresAt"`
|
|
RefreshedAfter int64 `json:"refreshedAfter"`
|
|
}
|
|
|
|
// PlayerKeyPair 玩家证书密钥对
|
|
type PlayerKeyPair struct {
|
|
PrivateKey string `json:"privateKey"`
|
|
PublicKey string `json:"publicKey"`
|
|
}
|
|
|
|
// yggdrasilCertificateService 证书服务实现
|
|
type yggdrasilCertificateService struct {
|
|
profileRepo repository.ProfileRepository
|
|
signatureService *SignatureService
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewCertificateService 创建证书服务实例
|
|
func NewCertificateService(
|
|
profileRepo repository.ProfileRepository,
|
|
signatureService *SignatureService,
|
|
logger *zap.Logger,
|
|
) CertificateService {
|
|
return &yggdrasilCertificateService{
|
|
profileRepo: profileRepo,
|
|
signatureService: signatureService,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// GeneratePlayerCertificate 生成玩家证书
|
|
func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Context, uuid string) (*PlayerCertificate, error) {
|
|
if uuid == "" {
|
|
return nil, apperrors.ErrUUIDRequired
|
|
}
|
|
|
|
s.logger.Info("开始生成玩家证书",
|
|
zap.String("uuid", uuid),
|
|
)
|
|
|
|
// 获取密钥对
|
|
keyPair, err := s.profileRepo.GetKeyPair(ctx, uuid)
|
|
if err != nil {
|
|
s.logger.Info("获取用户密钥对失败,将创建新密钥对",
|
|
zap.Error(err),
|
|
zap.String("uuid", uuid),
|
|
)
|
|
keyPair = nil
|
|
}
|
|
|
|
// 如果没有找到密钥对或密钥对已过期,创建一个新的
|
|
now := time.Now().UTC()
|
|
if keyPair == nil || keyPair.Refresh.Before(now) || keyPair.PrivateKey == "" || keyPair.PublicKey == "" {
|
|
s.logger.Info("为用户创建新的密钥对",
|
|
zap.String("uuid", uuid),
|
|
)
|
|
keyPair, err = s.signatureService.NewKeyPair(ctx)
|
|
if err != nil {
|
|
s.logger.Error("生成玩家证书密钥对失败",
|
|
zap.Error(err),
|
|
zap.String("uuid", uuid),
|
|
)
|
|
return nil, fmt.Errorf("生成玩家证书密钥对失败: %w", err)
|
|
}
|
|
|
|
// 保存密钥对到数据库
|
|
err = s.profileRepo.UpdateKeyPair(ctx, uuid, keyPair)
|
|
if err != nil {
|
|
s.logger.Warn("更新用户密钥对失败",
|
|
zap.Error(err),
|
|
zap.String("uuid", uuid),
|
|
)
|
|
// 继续执行,即使保存失败
|
|
}
|
|
}
|
|
|
|
// 计算expiresAt的毫秒时间戳
|
|
expiresAtMillis := keyPair.Expiration.UnixMilli()
|
|
|
|
// 返回玩家证书
|
|
certificate := &PlayerCertificate{
|
|
KeyPair: PlayerKeyPair{
|
|
PrivateKey: keyPair.PrivateKey,
|
|
PublicKey: keyPair.PublicKey,
|
|
},
|
|
PublicKeySignature: keyPair.PublicKeySignature,
|
|
PublicKeySignatureV2: keyPair.PublicKeySignatureV2,
|
|
ExpiresAt: expiresAtMillis,
|
|
RefreshedAfter: keyPair.Refresh.UnixMilli(),
|
|
}
|
|
|
|
s.logger.Info("成功生成玩家证书",
|
|
zap.String("uuid", uuid),
|
|
)
|
|
return certificate, nil
|
|
}
|
|
|
|
// GetPublicKey 获取公钥
|
|
func (s *yggdrasilCertificateService) GetPublicKey(ctx context.Context) (string, error) {
|
|
return s.signatureService.GetPublicKeyFromRedis(ctx)
|
|
}
|