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,与前端约定对齐。
180 lines
4.6 KiB
Go
180 lines
4.6 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net"
|
||
"strings"
|
||
"time"
|
||
|
||
apperrors "carrotskin/internal/errors"
|
||
"carrotskin/pkg/redis"
|
||
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// SessionKeyPrefix Redis会话键前缀
|
||
const SessionKeyPrefix = "Join_"
|
||
|
||
// SessionTTL 会话超时时间 - 增加到15分钟
|
||
const SessionTTL = 15 * time.Minute
|
||
|
||
// SessionData 会话数据
|
||
type SessionData struct {
|
||
AccessToken string `json:"accessToken"`
|
||
UserName string `json:"userName"`
|
||
SelectedProfile string `json:"selectedProfile"`
|
||
IP string `json:"ip"`
|
||
}
|
||
|
||
// SessionService 会话管理服务接口
|
||
type SessionService interface {
|
||
// CreateSession 创建服务器会话
|
||
CreateSession(ctx context.Context, serverID, accessToken, username, profileUUID, ip string) error
|
||
// GetSession 获取会话数据
|
||
GetSession(ctx context.Context, serverID string) (*SessionData, error)
|
||
// ValidateSession 验证会话(用户名和IP)
|
||
ValidateSession(ctx context.Context, serverID, username, ip string) error
|
||
}
|
||
|
||
// yggdrasilSessionService 会话服务实现
|
||
type yggdrasilSessionService struct {
|
||
redis *redis.Client
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// NewSessionService 创建会话服务实例
|
||
func NewSessionService(redisClient *redis.Client, logger *zap.Logger) SessionService {
|
||
return &yggdrasilSessionService{
|
||
redis: redisClient,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
// ValidateServerID 验证服务器ID格式
|
||
func ValidateServerID(serverID string) error {
|
||
if serverID == "" {
|
||
return apperrors.ErrInvalidServerID
|
||
}
|
||
if len(serverID) > 100 || strings.ContainsAny(serverID, "<>\"'&") {
|
||
return apperrors.ErrInvalidServerID
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValidateIP 验证IP地址格式
|
||
func ValidateIP(ip string) error {
|
||
if ip == "" {
|
||
return nil // IP是可选的
|
||
}
|
||
if net.ParseIP(ip) == nil {
|
||
return apperrors.ErrIPMismatch
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CreateSession 创建服务器会话
|
||
func (s *yggdrasilSessionService) CreateSession(ctx context.Context, serverID, accessToken, username, profileUUID, ip string) error {
|
||
// 输入验证
|
||
if err := ValidateServerID(serverID); err != nil {
|
||
return err
|
||
}
|
||
if accessToken == "" {
|
||
return apperrors.ErrInvalidAccessToken
|
||
}
|
||
if username == "" {
|
||
return apperrors.ErrUsernameRequired
|
||
}
|
||
if profileUUID == "" {
|
||
return apperrors.ErrProfileMismatch
|
||
}
|
||
if err := ValidateIP(ip); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 创建会话数据
|
||
data := SessionData{
|
||
AccessToken: accessToken,
|
||
UserName: username,
|
||
SelectedProfile: profileUUID,
|
||
IP: ip,
|
||
}
|
||
|
||
// 序列化会话数据
|
||
marshaledData, err := json.Marshal(data)
|
||
if err != nil {
|
||
s.logger.Error("序列化会话数据失败",
|
||
zap.Error(err),
|
||
zap.String("serverID", serverID),
|
||
)
|
||
return fmt.Errorf("序列化会话数据失败: %w", err)
|
||
}
|
||
|
||
// 存储会话数据到Redis
|
||
sessionKey := SessionKeyPrefix + serverID
|
||
if err = s.redis.Set(ctx, sessionKey, marshaledData, SessionTTL); err != nil {
|
||
s.logger.Error("保存会话数据失败",
|
||
zap.Error(err),
|
||
zap.String("serverID", serverID),
|
||
)
|
||
return fmt.Errorf("保存会话数据失败: %w", err)
|
||
}
|
||
|
||
s.logger.Info("会话创建成功",
|
||
zap.String("username", username),
|
||
zap.String("serverID", serverID),
|
||
)
|
||
return nil
|
||
}
|
||
|
||
// GetSession 获取会话数据(内部调用前已对 serverID 校验过,跳过重复校验)
|
||
func (s *yggdrasilSessionService) GetSession(ctx context.Context, serverID string) (*SessionData, error) {
|
||
// 从Redis获取会话数据
|
||
sessionKey := SessionKeyPrefix + serverID
|
||
data, err := s.redis.GetBytes(ctx, sessionKey)
|
||
if err != nil {
|
||
s.logger.Error("获取会话数据失败",
|
||
zap.Error(err),
|
||
zap.String("serverID", serverID),
|
||
)
|
||
return nil, fmt.Errorf("获取会话数据失败: %w", err)
|
||
}
|
||
|
||
// 反序列化会话数据
|
||
var sessionData SessionData
|
||
if err = json.Unmarshal(data, &sessionData); err != nil {
|
||
s.logger.Error("解析会话数据失败",
|
||
zap.Error(err),
|
||
zap.String("serverID", serverID),
|
||
)
|
||
return nil, fmt.Errorf("解析会话数据失败: %w", err)
|
||
}
|
||
|
||
return &sessionData, nil
|
||
}
|
||
|
||
// ValidateSession 验证会话(用户名和IP)
|
||
func (s *yggdrasilSessionService) ValidateSession(ctx context.Context, serverID, username, ip string) error {
|
||
if serverID == "" || username == "" {
|
||
return apperrors.ErrSessionMismatch
|
||
}
|
||
|
||
// ValidateSession 由 HasJoinedServer 调用,serverID 已经过校验,此处无需重复 ValidateServerID
|
||
sessionData, err := s.GetSession(ctx, serverID)
|
||
if err != nil {
|
||
return apperrors.ErrSessionNotFound
|
||
}
|
||
|
||
// 验证用户名
|
||
if sessionData.UserName != username {
|
||
return apperrors.ErrUsernameMismatch
|
||
}
|
||
|
||
// 验证IP(如果提供)
|
||
if ip != "" && sessionData.IP != ip {
|
||
return apperrors.ErrIPMismatch
|
||
}
|
||
|
||
return nil
|
||
}
|