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:
@@ -1,29 +1,13 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
YggdrasilPrivateKeyRedisKey = "yggdrasil:private_key"
|
||||
)
|
||||
|
||||
// RedisClient 定义Redis客户端接口(用于测试)
|
||||
type RedisClient interface {
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error
|
||||
}
|
||||
|
||||
// YggdrasilJWTService Yggdrasil JWT服务(使用RSA512)
|
||||
type YggdrasilJWTService struct {
|
||||
privateKey *rsa.PrivateKey
|
||||
@@ -122,98 +106,3 @@ func (j *YggdrasilJWTService) ParseAccessToken(accessToken string, stalePolicy S
|
||||
func (j *YggdrasilJWTService) GetPublicKey() *rsa.PublicKey {
|
||||
return j.publicKey
|
||||
}
|
||||
|
||||
// YggdrasilJWTManager Yggdrasil JWT管理器,用于获取或创建JWT服务
|
||||
type YggdrasilJWTManager struct {
|
||||
redisClient RedisClient
|
||||
jwtService *YggdrasilJWTService
|
||||
privateKey *rsa.PrivateKey
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewYggdrasilJWTManager 创建Yggdrasil JWT管理器
|
||||
func NewYggdrasilJWTManager(redisClient RedisClient) *YggdrasilJWTManager {
|
||||
return &YggdrasilJWTManager{
|
||||
redisClient: redisClient,
|
||||
}
|
||||
}
|
||||
|
||||
// GetJWTService 获取或创建Yggdrasil JWT服务(线程安全)
|
||||
func (m *YggdrasilJWTManager) GetJWTService() (*YggdrasilJWTService, error) {
|
||||
m.mu.RLock()
|
||||
if m.jwtService != nil {
|
||||
service := m.jwtService
|
||||
m.mu.RUnlock()
|
||||
return service, nil
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// 双重检查
|
||||
if m.jwtService != nil {
|
||||
return m.jwtService, nil
|
||||
}
|
||||
|
||||
// 从Redis获取私钥
|
||||
privateKey, err := m.getPrivateKeyFromRedis()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取私钥失败: %w", err)
|
||||
}
|
||||
|
||||
m.privateKey = privateKey
|
||||
m.jwtService = NewYggdrasilJWTService(privateKey, "carrotskin")
|
||||
return m.jwtService, nil
|
||||
}
|
||||
|
||||
// SetPrivateKey 直接设置私钥(用于测试或直接从signatureService获取)
|
||||
func (m *YggdrasilJWTManager) SetPrivateKey(privateKey *rsa.PrivateKey) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.privateKey = privateKey
|
||||
if privateKey != nil {
|
||||
m.jwtService = NewYggdrasilJWTService(privateKey, "carrotskin")
|
||||
}
|
||||
}
|
||||
|
||||
// getPrivateKeyFromRedis 从Redis获取私钥
|
||||
func (m *YggdrasilJWTManager) getPrivateKeyFromRedis() (*rsa.PrivateKey, error) {
|
||||
if m.privateKey != nil {
|
||||
return m.privateKey, nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
privateKeyPEM, err := m.redisClient.Get(ctx, YggdrasilPrivateKeyRedisKey)
|
||||
if err != nil || privateKeyPEM == "" {
|
||||
return nil, fmt.Errorf("从Redis获取私钥失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析PEM格式的私钥
|
||||
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 privateKey, nil
|
||||
}
|
||||
|
||||
// GenerateKeyPair 生成RSA密钥对(用于测试)
|
||||
func GenerateKeyPair() (*rsa.PrivateKey, error) {
|
||||
return rsa.GenerateKey(rand.Reader, 2048)
|
||||
}
|
||||
|
||||
// EncodePrivateKeyToPEM 将私钥编码为PEM格式(用于测试)
|
||||
func EncodePrivateKeyToPEM(privateKey *rsa.PrivateKey) (string, error) {
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: privateKeyBytes,
|
||||
})
|
||||
return string(privateKeyPEM), nil
|
||||
}
|
||||
|
||||
@@ -1,65 +1,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// MockRedisClient 模拟Redis客户端
|
||||
type MockRedisClient struct {
|
||||
data map[string]string
|
||||
err error
|
||||
}
|
||||
|
||||
func NewMockRedisClient() *MockRedisClient {
|
||||
return &MockRedisClient{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Get(ctx context.Context, key string) (string, error) {
|
||||
if m.err != nil {
|
||||
return "", m.err
|
||||
}
|
||||
if val, ok := m.data[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return "", redis.Nil
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
if m.err != nil {
|
||||
return m.err
|
||||
}
|
||||
m.data[key] = value.(string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) SetError(err error) {
|
||||
m.err = err
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) ClearError() {
|
||||
m.err = nil
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) SetData(key, value string) {
|
||||
m.data[key] = value
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Clear() {
|
||||
m.data = make(map[string]string)
|
||||
m.err = nil
|
||||
}
|
||||
|
||||
// 测试辅助函数:生成测试用的密钥对
|
||||
// generateTestKeyPair 生成测试用的 RSA 密钥对
|
||||
func generateTestKeyPair(t *testing.T) *rsa.PrivateKey {
|
||||
privateKey, err := GenerateKeyPair()
|
||||
t.Helper()
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("生成密钥对失败: %v", err)
|
||||
}
|
||||
@@ -285,169 +236,6 @@ func TestYggdrasilJWTService_GetPublicKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewYggdrasilJWTManager(t *testing.T) {
|
||||
mockRedis := NewMockRedisClient()
|
||||
manager := NewYggdrasilJWTManager(mockRedis)
|
||||
|
||||
if manager == nil {
|
||||
t.Fatal("管理器创建失败")
|
||||
}
|
||||
if manager.redisClient != mockRedis {
|
||||
t.Error("Redis客户端未正确设置")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYggdrasilJWTManager_SetPrivateKey(t *testing.T) {
|
||||
mockRedis := NewMockRedisClient()
|
||||
manager := NewYggdrasilJWTManager(mockRedis)
|
||||
|
||||
privateKey := generateTestKeyPair(t)
|
||||
manager.SetPrivateKey(privateKey)
|
||||
|
||||
// 验证JWT服务已创建
|
||||
service, err := manager.GetJWTService()
|
||||
if err != nil {
|
||||
t.Fatalf("获取JWT服务失败: %v", err)
|
||||
}
|
||||
if service == nil {
|
||||
t.Fatal("JWT服务不应为nil")
|
||||
}
|
||||
// 验证服务可以正常工作
|
||||
if service.GetPublicKey() == nil {
|
||||
t.Error("公钥不应为nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYggdrasilJWTManager_GetJWTService_FromPrivateKey(t *testing.T) {
|
||||
mockRedis := NewMockRedisClient()
|
||||
manager := NewYggdrasilJWTManager(mockRedis)
|
||||
|
||||
privateKey := generateTestKeyPair(t)
|
||||
manager.SetPrivateKey(privateKey)
|
||||
|
||||
// 第一次获取
|
||||
service1, err := manager.GetJWTService()
|
||||
if err != nil {
|
||||
t.Fatalf("获取JWT服务失败: %v", err)
|
||||
}
|
||||
|
||||
// 第二次获取应该返回同一个实例
|
||||
service2, err := manager.GetJWTService()
|
||||
if err != nil {
|
||||
t.Fatalf("获取JWT服务失败: %v", err)
|
||||
}
|
||||
|
||||
if service1 != service2 {
|
||||
t.Error("应该返回同一个JWT服务实例")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYggdrasilJWTManager_GetJWTService_FromRedis(t *testing.T) {
|
||||
mockRedis := NewMockRedisClient()
|
||||
manager := NewYggdrasilJWTManager(mockRedis)
|
||||
|
||||
privateKey := generateTestKeyPair(t)
|
||||
privateKeyPEM, err := EncodePrivateKeyToPEM(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("编码私钥失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置Redis数据
|
||||
mockRedis.SetData(YggdrasilPrivateKeyRedisKey, privateKeyPEM)
|
||||
|
||||
// 获取JWT服务
|
||||
service, err := manager.GetJWTService()
|
||||
if err != nil {
|
||||
t.Fatalf("获取JWT服务失败: %v", err)
|
||||
}
|
||||
if service == nil {
|
||||
t.Error("JWT服务不应为nil")
|
||||
}
|
||||
|
||||
// 验证服务可以正常工作
|
||||
token, err := service.GenerateAccessToken(123, "client-uuid", 1, "profile-uuid",
|
||||
time.Now().Add(24*time.Hour), time.Now().Add(30*24*time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("生成Token失败: %v", err)
|
||||
}
|
||||
if token == "" {
|
||||
t.Error("Token不应为空")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYggdrasilJWTManager_GetJWTService_RedisError(t *testing.T) {
|
||||
mockRedis := NewMockRedisClient()
|
||||
manager := NewYggdrasilJWTManager(mockRedis)
|
||||
|
||||
// 设置Redis错误
|
||||
mockRedis.SetError(errors.New("redis connection error"))
|
||||
|
||||
// 尝试获取JWT服务应该失败
|
||||
_, err := manager.GetJWTService()
|
||||
if err == nil {
|
||||
t.Error("期望出现错误,但没有错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYggdrasilJWTManager_GetJWTService_InvalidPEM(t *testing.T) {
|
||||
mockRedis := NewMockRedisClient()
|
||||
manager := NewYggdrasilJWTManager(mockRedis)
|
||||
|
||||
// 设置无效的PEM数据
|
||||
mockRedis.SetData(YggdrasilPrivateKeyRedisKey, "invalid-pem-data")
|
||||
|
||||
// 尝试获取JWT服务应该失败
|
||||
_, err := manager.GetJWTService()
|
||||
if err == nil {
|
||||
t.Error("期望出现错误,但没有错误")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYggdrasilJWTManager_GetJWTService_Concurrent(t *testing.T) {
|
||||
mockRedis := NewMockRedisClient()
|
||||
manager := NewYggdrasilJWTManager(mockRedis)
|
||||
|
||||
privateKey := generateTestKeyPair(t)
|
||||
privateKeyPEM, err := EncodePrivateKeyToPEM(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("编码私钥失败: %v", err)
|
||||
}
|
||||
|
||||
mockRedis.SetData(YggdrasilPrivateKeyRedisKey, privateKeyPEM)
|
||||
|
||||
// 并发获取JWT服务
|
||||
const numGoroutines = 10
|
||||
results := make(chan *YggdrasilJWTService, numGoroutines)
|
||||
errors := make(chan error, numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
service, err := manager.GetJWTService()
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
results <- service
|
||||
}()
|
||||
}
|
||||
|
||||
// 收集结果
|
||||
services := make(map[*YggdrasilJWTService]bool)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
select {
|
||||
case service := <-results:
|
||||
services[service] = true
|
||||
case err := <-errors:
|
||||
t.Fatalf("获取JWT服务失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 所有goroutine应该返回同一个服务实例
|
||||
if len(services) != 1 {
|
||||
t.Errorf("期望所有goroutine返回同一个服务实例,但得到 %d 个不同的实例", len(services))
|
||||
}
|
||||
}
|
||||
|
||||
func TestYggdrasilTokenClaims_EmptyProfileID(t *testing.T) {
|
||||
privateKey := generateTestKeyPair(t)
|
||||
service := NewYggdrasilJWTService(privateKey, "test-issuer")
|
||||
|
||||
Reference in New Issue
Block a user