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:
@@ -12,7 +12,7 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"carrotskin/internal/model"
|
||||
@@ -38,6 +38,11 @@ type SignatureService struct {
|
||||
profileRepo repository.ProfileRepository
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
|
||||
// 缓存已解析的根密钥对,避免每次签名都重复 PEM 解析(PKCS1 解析开销较大)
|
||||
rootKeyMu sync.RWMutex
|
||||
rootPrivateKey *rsa.PrivateKey
|
||||
rootPublicKeyPEM string
|
||||
}
|
||||
|
||||
// NewSignatureService 创建SignatureService实例
|
||||
@@ -91,74 +96,77 @@ func (s *SignatureService) NewKeyPair(ctx context.Context) (*model.KeyPair, erro
|
||||
return nil, fmt.Errorf("获取Yggdrasil根密钥失败: %w", err)
|
||||
}
|
||||
|
||||
// 构造签名消息
|
||||
// 构造签名消息(PEM 公钥 + 过期时间戳)
|
||||
expiresAtMillis := expiration.UnixMilli()
|
||||
message := []byte(string(publicKeyPEM) + strconv.FormatInt(expiresAtMillis, 10))
|
||||
message := make([]byte, 0, len(publicKeyPEM)+20)
|
||||
message = append(message, publicKeyPEM...)
|
||||
message = strconv.AppendInt(message, expiresAtMillis, 10)
|
||||
|
||||
// 使用SHA1withRSA签名
|
||||
hashed := sha1.Sum(message)
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, yggPrivateKey, crypto.SHA1, hashed[:])
|
||||
publicKeySignature, err := signWithRootKey(yggPrivateKey, message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("签名失败: %w", err)
|
||||
}
|
||||
publicKeySignature := base64.StdEncoding.EncodeToString(signature)
|
||||
|
||||
// 构造V2签名消息(DER编码)
|
||||
publicKeyDER, err := x509.MarshalPKIXPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DER编码公钥失败: %w", err)
|
||||
}
|
||||
|
||||
// V2签名:timestamp (8 bytes, big endian) + publicKey (DER)
|
||||
messageV2 := make([]byte, 8+len(publicKeyDER))
|
||||
messageV2 := make([]byte, 8+len(publicKeyBytes))
|
||||
binary.BigEndian.PutUint64(messageV2[0:8], uint64(expiresAtMillis))
|
||||
copy(messageV2[8:], publicKeyDER)
|
||||
copy(messageV2[8:], publicKeyBytes)
|
||||
|
||||
hashedV2 := sha1.Sum(messageV2)
|
||||
signatureV2, err := rsa.SignPKCS1v15(rand.Reader, yggPrivateKey, crypto.SHA1, hashedV2[:])
|
||||
publicKeySignatureV2, err := signWithRootKey(yggPrivateKey, messageV2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("V2签名失败: %w", err)
|
||||
}
|
||||
publicKeySignatureV2 := base64.StdEncoding.EncodeToString(signatureV2)
|
||||
|
||||
return &model.KeyPair{
|
||||
PrivateKey: string(privateKeyPEM),
|
||||
PublicKey: string(publicKeyPEM),
|
||||
PublicKeySignature: publicKeySignature,
|
||||
PublicKeySignatureV2: publicKeySignatureV2,
|
||||
PublicKeySignature: base64.StdEncoding.EncodeToString(publicKeySignature),
|
||||
PublicKeySignatureV2: base64.StdEncoding.EncodeToString(publicKeySignatureV2),
|
||||
YggdrasilPublicKey: yggPublicKey,
|
||||
Expiration: expiration,
|
||||
Refresh: refresh,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// signWithRootKey 使用根密钥对消息做 SHA1withRSA 签名
|
||||
func signWithRootKey(privateKey *rsa.PrivateKey, message []byte) ([]byte, error) {
|
||||
hashed := sha1.Sum(message)
|
||||
return rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hashed[:])
|
||||
}
|
||||
|
||||
// GetOrCreateYggdrasilKeyPair 获取或创建Yggdrasil根密钥对
|
||||
// 优先使用进程内缓存的已解析私钥;缓存未命中时从Redis加载并解析,再缓存。
|
||||
func (s *SignatureService) GetOrCreateYggdrasilKeyPair(ctx context.Context) (string, *rsa.PrivateKey, error) {
|
||||
// 尝试从Redis获取密钥
|
||||
publicKeyPEM, err := s.redis.Get(ctx, PublicKeyRedisKey)
|
||||
if err == nil && publicKeyPEM != "" {
|
||||
privateKeyPEM, err := s.redis.Get(ctx, PrivateKeyRedisKey)
|
||||
if err == nil && privateKeyPEM != "" {
|
||||
// 检查密钥是否过期
|
||||
expStr, err := s.redis.Get(ctx, KeyExpirationRedisKey)
|
||||
if err == nil && expStr != "" {
|
||||
expTime, err := time.Parse(time.RFC3339, expStr)
|
||||
if err == nil && time.Now().Before(expTime) {
|
||||
// 密钥有效,解析私钥
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block != nil {
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err == nil {
|
||||
s.logger.Info("从Redis加载Yggdrasil根密钥")
|
||||
return publicKeyPEM, privateKey, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 1. 读缓存(快路径)
|
||||
s.rootKeyMu.RLock()
|
||||
if s.rootPrivateKey != nil && s.isRootKeyValid() {
|
||||
pub := s.rootPublicKeyPEM
|
||||
priv := s.rootPrivateKey
|
||||
s.rootKeyMu.RUnlock()
|
||||
return pub, priv, nil
|
||||
}
|
||||
s.rootKeyMu.RUnlock()
|
||||
|
||||
// 2. 缓存未命中,加锁准备从 Redis 加载/生成
|
||||
s.rootKeyMu.Lock()
|
||||
defer s.rootKeyMu.Unlock()
|
||||
|
||||
// 双重检查:可能在等待锁期间已被其他 goroutine 填充
|
||||
if s.rootPrivateKey != nil && s.isRootKeyValidLocked() {
|
||||
return s.rootPublicKeyPEM, s.rootPrivateKey, nil
|
||||
}
|
||||
|
||||
// 生成新的根密钥对
|
||||
// 尝试从Redis获取密钥(MGet 一次往返拿三个字段,减少网络往返)
|
||||
pub, priv, err := s.loadRootKeyFromRedis(ctx)
|
||||
if err == nil && priv != nil {
|
||||
s.rootPrivateKey = priv
|
||||
s.rootPublicKeyPEM = pub
|
||||
s.logger.Info("从Redis加载Yggdrasil根密钥")
|
||||
return pub, priv, nil
|
||||
}
|
||||
|
||||
// Redis 没有,生成新的根密钥对
|
||||
s.logger.Info("生成新的Yggdrasil根密钥对")
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
||||
if err != nil {
|
||||
@@ -177,7 +185,7 @@ func (s *SignatureService) GetOrCreateYggdrasilKeyPair(ctx context.Context) (str
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("编码公钥失败: %w", err)
|
||||
}
|
||||
publicKeyPEM = string(pem.EncodeToMemory(&pem.Block{
|
||||
publicKeyPEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: publicKeyBytes,
|
||||
}))
|
||||
@@ -196,9 +204,57 @@ func (s *SignatureService) GetOrCreateYggdrasilKeyPair(ctx context.Context) (str
|
||||
s.logger.Warn("保存密钥过期时间到Redis失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 缓存已解析的私钥与公钥PEM
|
||||
s.rootPrivateKey = privateKey
|
||||
s.rootPublicKeyPEM = publicKeyPEM
|
||||
return publicKeyPEM, privateKey, nil
|
||||
}
|
||||
|
||||
// loadRootKeyFromRedis 从Redis加载根密钥,使用 MGet 单次往返获取三字段
|
||||
func (s *SignatureService) loadRootKeyFromRedis(ctx context.Context) (string, *rsa.PrivateKey, error) {
|
||||
results, err := s.redis.MGet(ctx, PublicKeyRedisKey, PrivateKeyRedisKey, KeyExpirationRedisKey).Result()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if len(results) < 3 {
|
||||
return "", nil, fmt.Errorf("MGet 返回字段数不足")
|
||||
}
|
||||
|
||||
publicKeyPEM, _ := results[0].(string)
|
||||
privateKeyPEM, _ := results[1].(string)
|
||||
expStr, _ := results[2].(string)
|
||||
|
||||
if publicKeyPEM == "" || privateKeyPEM == "" || expStr == "" {
|
||||
return "", nil, fmt.Errorf("Redis中密钥字段不完整")
|
||||
}
|
||||
|
||||
expTime, err := time.Parse(time.RFC3339, expStr)
|
||||
if err != nil || !time.Now().Before(expTime) {
|
||||
return "", nil, fmt.Errorf("密钥已过期或过期时间解析失败")
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil {
|
||||
return "", nil, fmt.Errorf("解析PEM私钥失败")
|
||||
}
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("解析RSA私钥失败: %w", err)
|
||||
}
|
||||
return publicKeyPEM, privateKey, nil
|
||||
}
|
||||
|
||||
// isRootKeyValid 在持有读锁时判断缓存密钥是否过期。
|
||||
// 注意:缓存的私钥本身不带过期时间,过期信息存于 Redis(由 loadRootKeyFromRedis 校验)。
|
||||
// 一旦加载成功即认为在进程生命周期内有效,由 Redis 过期触发重新生成。
|
||||
func (s *SignatureService) isRootKeyValid() bool {
|
||||
return s.isRootKeyValidLocked()
|
||||
}
|
||||
|
||||
func (s *SignatureService) isRootKeyValidLocked() bool {
|
||||
return s.rootPrivateKey != nil && s.rootPublicKeyPEM != ""
|
||||
}
|
||||
|
||||
// GetPublicKeyFromRedis 从Redis获取公钥
|
||||
func (s *SignatureService) GetPublicKeyFromRedis(ctx context.Context) (string, error) {
|
||||
publicKey, err := s.redis.Get(ctx, PublicKeyRedisKey)
|
||||
@@ -216,81 +272,16 @@ func (s *SignatureService) GetPublicKeyFromRedis(ctx context.Context) (string, e
|
||||
}
|
||||
|
||||
// SignStringWithSHA1withRSA 使用SHA1withRSA签名字符串
|
||||
// 使用缓存的已解析根私钥;若缓存为空则触发加载/生成。
|
||||
func (s *SignatureService) SignStringWithSHA1withRSA(ctx context.Context, data string) (string, error) {
|
||||
|
||||
// 从Redis获取私钥
|
||||
privateKeyPEM, err := s.redis.Get(ctx, PrivateKeyRedisKey)
|
||||
if err != nil || privateKeyPEM == "" {
|
||||
// 如果没有私钥,创建新的密钥对
|
||||
_, privateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取私钥失败: %w", err)
|
||||
}
|
||||
// 使用新生成的私钥签名
|
||||
hashed := sha1.Sum([]byte(data))
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hashed[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("签名失败: %w", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// 解析PEM格式的私钥
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("解析PEM私钥失败")
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
_, privateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析RSA私钥失败: %w", err)
|
||||
return "", fmt.Errorf("获取签名私钥失败: %w", err)
|
||||
}
|
||||
|
||||
// 签名
|
||||
hashed := sha1.Sum([]byte(data))
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hashed[:])
|
||||
signature, err := signWithRootKey(privateKey, []byte(data))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("签名失败: %w", err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// FormatPublicKey 格式化公钥为单行格式(去除PEM头尾和换行符)
|
||||
func FormatPublicKey(publicKeyPEM string) string {
|
||||
// 移除PEM格式的头尾
|
||||
lines := strings.Split(publicKeyPEM, "\n")
|
||||
var keyLines []string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed != "" &&
|
||||
!strings.HasPrefix(trimmed, "-----BEGIN") &&
|
||||
!strings.HasPrefix(trimmed, "-----END") {
|
||||
keyLines = append(keyLines, trimmed)
|
||||
}
|
||||
}
|
||||
return strings.Join(keyLines, "")
|
||||
}
|
||||
|
||||
// SignStringWithProfileRSA 使用Profile的RSA私钥签名字符串
|
||||
func (s *SignatureService) SignStringWithProfileRSA(data string, privateKeyPEM string) (string, error) {
|
||||
// 解析PEM格式的私钥
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("解析PEM私钥失败")
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析RSA私钥失败: %w", err)
|
||||
}
|
||||
|
||||
// 签名
|
||||
hashed := sha1.Sum([]byte(data))
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hashed[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("签名失败: %w", err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user