refactor: 重构服务层和仓库层

This commit is contained in:
lan
2025-12-03 10:58:39 +08:00
parent 034e02e93a
commit e873c58af9
32 changed files with 1445 additions and 5213 deletions

View File

@@ -18,7 +18,6 @@ import (
var (
slideTileCapt slide.Captcha
cfg *config.Config
)
// 常量定义业务相关配置与Redis连接配置分离
@@ -29,8 +28,6 @@ const (
// Init 验证码图初始化
func init() {
cfg, _ = config.Load()
// 从默认仓库中获取主图
builder := slide.NewBuilder()
bgImage, err := imagesv2.GetImages()
if err != nil {

View File

@@ -58,6 +58,7 @@ type TextureService interface {
// 材质CRUD
Create(ctx context.Context, uploaderID int64, name, description, textureType, url, hash string, size int, isPublic, isSlim bool) (*model.Texture, error)
GetByID(ctx context.Context, id int64) (*model.Texture, error)
GetByHash(ctx context.Context, hash string) (*model.Texture, error)
GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error)
Search(ctx context.Context, keyword string, textureType model.TextureType, publicOnly bool, page, pageSize int) ([]*model.Texture, int64, error)
Update(ctx context.Context, textureID, uploaderID int64, name, description string, isPublic *bool) (*model.Texture, error)

View File

@@ -120,6 +120,37 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
return texture2, nil
}
func (s *textureService) GetByHash(ctx context.Context, hash string) (*model.Texture, error) {
// 尝试从缓存获取
cacheKey := s.cacheKeys.TextureByHash(hash)
var texture model.Texture
if err := s.cache.Get(ctx, cacheKey, &texture); err == nil {
if texture.Status == -1 {
return nil, errors.New("材质已删除")
}
return &texture, nil
}
// 缓存未命中,从数据库查询
texture2, err := s.textureRepo.FindByHash(hash)
if err != nil {
return nil, err
}
if texture2 == nil {
return nil, ErrTextureNotFound
}
if texture2.Status == -1 {
return nil, errors.New("材质已删除")
}
// 存入缓存异步5分钟过期
go func() {
_ = s.cache.Set(context.Background(), cacheKey, texture2, 5*time.Minute)
}()
return texture2, nil
}
func (s *textureService) GetByUserID(ctx context.Context, uploaderID int64, page, pageSize int) ([]*model.Texture, int64, error) {
page, pageSize = NormalizePagination(page, pageSize)

View File

@@ -0,0 +1,91 @@
package service
import (
apperrors "carrotskin/internal/errors"
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/auth"
"context"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
)
// yggdrasilAuthService Yggdrasil认证服务实现
// 负责认证和密码管理
type yggdrasilAuthService struct {
db *gorm.DB
userRepo repository.UserRepository
yggdrasilRepo repository.YggdrasilRepository
logger *zap.Logger
}
// NewYggdrasilAuthService 创建Yggdrasil认证服务实例内部使用
func NewYggdrasilAuthService(
db *gorm.DB,
userRepo repository.UserRepository,
yggdrasilRepo repository.YggdrasilRepository,
logger *zap.Logger,
) *yggdrasilAuthService {
return &yggdrasilAuthService{
db: db,
userRepo: userRepo,
yggdrasilRepo: yggdrasilRepo,
logger: logger,
}
}
func (s *yggdrasilAuthService) GetUserIDByEmail(ctx context.Context, email string) (int64, error) {
user, err := s.userRepo.FindByEmail(email)
if err != nil {
return 0, apperrors.ErrUserNotFound
}
return user.ID, nil
}
func (s *yggdrasilAuthService) VerifyPassword(ctx context.Context, password string, userID int64) error {
passwordStore, err := s.yggdrasilRepo.GetPasswordByID(userID)
if err != nil {
return apperrors.ErrPasswordNotSet
}
// 使用 bcrypt 验证密码
if !auth.CheckPassword(passwordStore, password) {
return apperrors.ErrPasswordMismatch
}
return nil
}
func (s *yggdrasilAuthService) ResetYggdrasilPassword(ctx context.Context, userID int64) (string, error) {
// 生成新的16位随机密码明文返回给用户
plainPassword := model.GenerateRandomPassword(16)
// 使用 bcrypt 加密密码后存储
hashedPassword, err := auth.HashPassword(plainPassword)
if err != nil {
return "", fmt.Errorf("密码加密失败: %w", err)
}
// 检查Yggdrasil记录是否存在
_, err = s.yggdrasilRepo.GetPasswordByID(userID)
if err != nil {
// 如果不存在,创建新记录
yggdrasil := model.Yggdrasil{
ID: userID,
Password: hashedPassword,
}
if err := s.db.Create(&yggdrasil).Error; err != nil {
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
}
return plainPassword, nil
}
// 如果存在,更新密码(存储加密后的密码)
if err := s.yggdrasilRepo.ResetPassword(userID, hashedPassword); err != nil {
return "", fmt.Errorf("重置Yggdrasil密码失败: %w", err)
}
// 返回明文密码给用户
return plainPassword, nil
}

View File

@@ -0,0 +1,112 @@
package service
import (
apperrors "carrotskin/internal/errors"
"carrotskin/internal/repository"
"context"
"fmt"
"time"
"go.uber.org/zap"
)
// CertificateService 证书服务接口
type CertificateService interface {
// GeneratePlayerCertificate 生成玩家证书
GeneratePlayerCertificate(ctx context.Context, uuid string) (map[string]interface{}, error)
// GetPublicKey 获取公钥
GetPublicKey(ctx context.Context) (string, error)
}
// 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) (map[string]interface{}, error) {
if uuid == "" {
return nil, apperrors.ErrUUIDRequired
}
s.logger.Info("开始生成玩家证书",
zap.String("uuid", uuid),
)
// 获取密钥对
keyPair, err := s.profileRepo.GetKeyPair(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()
if err != nil {
s.logger.Error("生成玩家证书密钥对失败",
zap.Error(err),
zap.String("uuid", uuid),
)
return nil, fmt.Errorf("生成玩家证书密钥对失败: %w", err)
}
// 保存密钥对到数据库
err = s.profileRepo.UpdateKeyPair(uuid, keyPair)
if err != nil {
s.logger.Warn("更新用户密钥对失败",
zap.Error(err),
zap.String("uuid", uuid),
)
// 继续执行,即使保存失败
}
}
// 计算expiresAt的毫秒时间戳
expiresAtMillis := keyPair.Expiration.UnixMilli()
// 返回玩家证书
certificate := map[string]interface{}{
"keyPair": map[string]interface{}{
"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()
}

View File

@@ -0,0 +1,156 @@
package service
import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"context"
"encoding/base64"
"time"
"go.uber.org/zap"
)
// SerializationService 序列化服务接口
type SerializationService interface {
// SerializeProfile 序列化档案为Yggdrasil格式
SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{}
// SerializeUser 序列化用户为Yggdrasil格式
SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{}
}
// Property Yggdrasil属性
type Property struct {
Name string `json:"name"`
Value string `json:"value"`
Signature string `json:"signature,omitempty"`
}
// yggdrasilSerializationService 序列化服务实现
type yggdrasilSerializationService struct {
textureRepo repository.TextureRepository
signatureService *signatureService
logger *zap.Logger
}
// NewSerializationService 创建序列化服务实例
func NewSerializationService(
textureRepo repository.TextureRepository,
signatureService *signatureService,
logger *zap.Logger,
) SerializationService {
return &yggdrasilSerializationService{
textureRepo: textureRepo,
signatureService: signatureService,
logger: logger,
}
}
// SerializeProfile 序列化档案为Yggdrasil格式
func (s *yggdrasilSerializationService) SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{} {
// 创建基本材质数据
texturesMap := make(map[string]interface{})
textures := map[string]interface{}{
"timestamp": time.Now().UnixMilli(),
"profileId": profile.UUID,
"profileName": profile.Name,
"textures": texturesMap,
}
// 处理皮肤
if profile.SkinID != nil {
skin, err := s.textureRepo.FindByID(*profile.SkinID)
if err != nil {
s.logger.Error("获取皮肤失败",
zap.Error(err),
zap.Int64("skinID", *profile.SkinID),
)
} else if skin != nil {
texturesMap["SKIN"] = map[string]interface{}{
"url": skin.URL,
"metadata": skin.Size,
}
}
}
// 处理披风
if profile.CapeID != nil {
cape, err := s.textureRepo.FindByID(*profile.CapeID)
if err != nil {
s.logger.Error("获取披风失败",
zap.Error(err),
zap.Int64("capeID", *profile.CapeID),
)
} else if cape != nil {
texturesMap["CAPE"] = map[string]interface{}{
"url": cape.URL,
"metadata": cape.Size,
}
}
}
// 将textures编码为base64
bytes, err := json.Marshal(textures)
if err != nil {
s.logger.Error("序列化textures失败",
zap.Error(err),
zap.String("profileUUID", profile.UUID),
)
return nil
}
textureData := base64.StdEncoding.EncodeToString(bytes)
signature, err := s.signatureService.SignStringWithSHA1withRSA(textureData)
if err != nil {
s.logger.Error("签名textures失败",
zap.Error(err),
zap.String("profileUUID", profile.UUID),
)
return nil
}
// 构建结果
data := map[string]interface{}{
"id": profile.UUID,
"name": profile.Name,
"properties": []Property{
{
Name: "textures",
Value: textureData,
Signature: signature,
},
},
}
return data
}
// SerializeUser 序列化用户为Yggdrasil格式
func (s *yggdrasilSerializationService) SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{} {
if user == nil {
s.logger.Error("尝试序列化空用户")
return nil
}
data := map[string]interface{}{
"id": uuid,
}
// 正确处理 *datatypes.JSON 指针类型
// 如果 Properties 为 nil则设置为 nil否则解引用并解析为 JSON 值
if user.Properties == nil {
data["properties"] = nil
} else {
// datatypes.JSON 是 []byte 类型,需要解析为实际的 JSON 值
var propertiesValue interface{}
if err := json.Unmarshal(*user.Properties, &propertiesValue); err != nil {
s.logger.Warn("解析用户Properties失败使用空值",
zap.Error(err),
zap.Int64("userID", user.ID),
)
data["properties"] = nil
} else {
data["properties"] = propertiesValue
}
}
return data
}

View File

@@ -1,402 +0,0 @@
package service
import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/auth"
"carrotskin/pkg/redis"
"carrotskin/pkg/utils"
"context"
"encoding/base64"
"errors"
"fmt"
"net"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)
// SessionKeyPrefix Redis会话键前缀
const SessionKeyPrefix = "Join_"
// SessionTTL 会话超时时间 - 增加到15分钟
const SessionTTL = 15 * time.Minute
type SessionData struct {
AccessToken string `json:"accessToken"`
UserName string `json:"userName"`
SelectedProfile string `json:"selectedProfile"`
IP string `json:"ip"`
}
// yggdrasilService YggdrasilService的实现
type yggdrasilService struct {
db *gorm.DB
userRepo repository.UserRepository
profileRepo repository.ProfileRepository
textureRepo repository.TextureRepository
tokenRepo repository.TokenRepository
yggdrasilRepo repository.YggdrasilRepository
signatureService *signatureService
redis *redis.Client
logger *zap.Logger
}
// NewYggdrasilService 创建YggdrasilService实例
func NewYggdrasilService(
db *gorm.DB,
userRepo repository.UserRepository,
profileRepo repository.ProfileRepository,
textureRepo repository.TextureRepository,
tokenRepo repository.TokenRepository,
yggdrasilRepo repository.YggdrasilRepository,
signatureService *signatureService,
redisClient *redis.Client,
logger *zap.Logger,
) YggdrasilService {
return &yggdrasilService{
db: db,
userRepo: userRepo,
profileRepo: profileRepo,
textureRepo: textureRepo,
tokenRepo: tokenRepo,
yggdrasilRepo: yggdrasilRepo,
signatureService: signatureService,
redis: redisClient,
logger: logger,
}
}
func (s *yggdrasilService) GetUserIDByEmail(ctx context.Context, email string) (int64, error) {
user, err := s.userRepo.FindByEmail(email)
if err != nil {
return 0, errors.New("用户不存在")
}
if user == nil {
return 0, errors.New("用户不存在")
}
return user.ID, nil
}
func (s *yggdrasilService) VerifyPassword(ctx context.Context, password string, userID int64) error {
passwordStore, err := s.yggdrasilRepo.GetPasswordByID(userID)
if err != nil {
return errors.New("未生成密码")
}
// 使用 bcrypt 验证密码
if !auth.CheckPassword(passwordStore, password) {
return errors.New("密码错误")
}
return nil
}
func (s *yggdrasilService) ResetYggdrasilPassword(ctx context.Context, userID int64) (string, error) {
// 生成新的16位随机密码明文返回给用户
plainPassword := model.GenerateRandomPassword(16)
// 使用 bcrypt 加密密码后存储
hashedPassword, err := auth.HashPassword(plainPassword)
if err != nil {
return "", fmt.Errorf("密码加密失败: %w", err)
}
// 检查Yggdrasil记录是否存在
_, err = s.yggdrasilRepo.GetPasswordByID(userID)
if err != nil {
// 如果不存在,创建新记录
yggdrasil := model.Yggdrasil{
ID: userID,
Password: hashedPassword,
}
if err := s.db.Create(&yggdrasil).Error; err != nil {
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
}
return plainPassword, nil
}
// 如果存在,更新密码(存储加密后的密码)
if err := s.yggdrasilRepo.ResetPassword(userID, hashedPassword); err != nil {
return "", fmt.Errorf("重置Yggdrasil密码失败: %w", err)
}
// 返回明文密码给用户
return plainPassword, nil
}
func (s *yggdrasilService) JoinServer(ctx context.Context, serverID, accessToken, selectedProfile, ip string) error {
// 输入验证
if serverID == "" || accessToken == "" || selectedProfile == "" {
return errors.New("参数不能为空")
}
// 验证serverId格式防止注入攻击
if len(serverID) > 100 || strings.ContainsAny(serverID, "<>\"'&") {
return errors.New("服务器ID格式无效")
}
// 验证IP格式
if ip != "" {
if net.ParseIP(ip) == nil {
return errors.New("IP地址格式无效")
}
}
// 获取和验证Token
token, err := s.tokenRepo.FindByAccessToken(accessToken)
if err != nil {
s.logger.Error(
"验证Token失败",
zap.Error(err),
zap.String("accessToken", accessToken),
)
return fmt.Errorf("验证Token失败: %w", err)
}
// 格式化UUID并验证与Token关联的配置文件
formattedProfile := utils.FormatUUID(selectedProfile)
if token.ProfileId != formattedProfile {
return errors.New("selectedProfile与Token不匹配")
}
profile, err := s.profileRepo.FindByUUID(formattedProfile)
if err != nil {
s.logger.Error(
"获取Profile失败",
zap.Error(err),
zap.String("uuid", formattedProfile),
)
return fmt.Errorf("获取Profile失败: %w", err)
}
// 创建会话数据
data := SessionData{
AccessToken: accessToken,
UserName: profile.Name,
SelectedProfile: formattedProfile,
IP: ip,
}
// 序列化会话数据
marshaledData, err := json.Marshal(data)
if err != nil {
s.logger.Error(
"[ERROR]序列化会话数据失败",
zap.Error(err),
)
return fmt.Errorf("序列化会话数据失败: %w", err)
}
// 存储会话数据到Redis - 使用传入的 ctx
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", profile.Name),
zap.String("serverId", serverID),
)
return nil
}
func (s *yggdrasilService) HasJoinedServer(ctx context.Context, serverID, username, ip string) error {
if serverID == "" || username == "" {
return errors.New("服务器ID和用户名不能为空")
}
// 从Redis获取会话数据 - 使用传入的 ctx
sessionKey := SessionKeyPrefix + serverID
data, err := s.redis.GetBytes(ctx, sessionKey)
if err != nil {
s.logger.Error("[ERROR] 获取会话数据失败:", zap.Error(err), zap.Any("serverId:", serverID))
return fmt.Errorf("获取会话数据失败: %w", err)
}
// 反序列化会话数据
var sessionData SessionData
if err = json.Unmarshal(data, &sessionData); err != nil {
s.logger.Error("[ERROR] 解析会话数据失败: ", zap.Error(err))
return fmt.Errorf("解析会话数据失败: %w", err)
}
// 验证用户名
if sessionData.UserName != username {
return errors.New("用户名不匹配")
}
// 验证IP(如果提供)
if ip != "" && sessionData.IP != ip {
return errors.New("IP地址不匹配")
}
return nil
}
func (s *yggdrasilService) SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{} {
// 创建基本材质数据
texturesMap := make(map[string]interface{})
textures := map[string]interface{}{
"timestamp": time.Now().UnixMilli(),
"profileId": profile.UUID,
"profileName": profile.Name,
"textures": texturesMap,
}
// 处理皮肤
if profile.SkinID != nil {
skin, err := s.textureRepo.FindByID(*profile.SkinID)
if err != nil {
s.logger.Error("[ERROR] 获取皮肤失败:", zap.Error(err), zap.Any("SkinID:", *profile.SkinID))
} else {
texturesMap["SKIN"] = map[string]interface{}{
"url": skin.URL,
"metadata": skin.Size,
}
}
}
// 处理披风
if profile.CapeID != nil {
cape, err := s.textureRepo.FindByID(*profile.CapeID)
if err != nil {
s.logger.Error("[ERROR] 获取披风失败:", zap.Error(err), zap.Any("capeID:", *profile.CapeID))
} else {
texturesMap["CAPE"] = map[string]interface{}{
"url": cape.URL,
"metadata": cape.Size,
}
}
}
// 将textures编码为base64
bytes, err := json.Marshal(textures)
if err != nil {
s.logger.Error("[ERROR] 序列化textures失败: ", zap.Error(err))
return nil
}
textureData := base64.StdEncoding.EncodeToString(bytes)
signature, err := s.signatureService.SignStringWithSHA1withRSA(textureData)
if err != nil {
s.logger.Error("[ERROR] 签名textures失败: ", zap.Error(err))
return nil
}
// 构建结果
data := map[string]interface{}{
"id": profile.UUID,
"name": profile.Name,
"properties": []Property{
{
Name: "textures",
Value: textureData,
Signature: signature,
},
},
}
return data
}
func (s *yggdrasilService) SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{} {
if user == nil {
s.logger.Error("[ERROR] 尝试序列化空用户")
return nil
}
data := map[string]interface{}{
"id": uuid,
}
// 正确处理 *datatypes.JSON 指针类型
// 如果 Properties 为 nil则设置为 nil否则解引用并解析为 JSON 值
if user.Properties == nil {
data["properties"] = nil
} else {
// datatypes.JSON 是 []byte 类型,需要解析为实际的 JSON 值
var propertiesValue interface{}
if err := json.Unmarshal(*user.Properties, &propertiesValue); err != nil {
s.logger.Warn("[WARN] 解析用户Properties失败使用空值", zap.Error(err))
data["properties"] = nil
} else {
data["properties"] = propertiesValue
}
}
return data
}
func (s *yggdrasilService) GeneratePlayerCertificate(ctx context.Context, uuid string) (map[string]interface{}, error) {
if uuid == "" {
return nil, fmt.Errorf("UUID不能为空")
}
s.logger.Info("[INFO] 开始生成玩家证书用户UUID: %s", zap.String("uuid", uuid))
keyPair, err := s.profileRepo.GetKeyPair(uuid)
if err != nil {
s.logger.Info("[INFO] 获取用户密钥对失败,将创建新密钥对: %v",
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("[INFO] 为用户创建新的密钥对: %s", zap.String("uuid", uuid))
keyPair, err = s.signatureService.NewKeyPair()
if err != nil {
s.logger.Error("[ERROR] 生成玩家证书密钥对失败: %v",
zap.Error(err),
zap.String("uuid", uuid),
)
return nil, fmt.Errorf("生成玩家证书密钥对失败: %w", err)
}
// 保存密钥对到数据库
err = s.profileRepo.UpdateKeyPair(uuid, keyPair)
if err != nil {
s.logger.Warn("[WARN] 更新用户密钥对失败: %v",
zap.Error(err),
zap.String("uuid", uuid),
)
// 继续执行,即使保存失败
}
}
// 计算expiresAt的毫秒时间戳
expiresAtMillis := keyPair.Expiration.UnixMilli()
// 返回玩家证书
certificate := map[string]interface{}{
"keyPair": map[string]interface{}{
"privateKey": keyPair.PrivateKey,
"publicKey": keyPair.PublicKey,
},
"publicKeySignature": keyPair.PublicKeySignature,
"publicKeySignatureV2": keyPair.PublicKeySignatureV2,
"expiresAt": expiresAtMillis,
"refreshedAfter": keyPair.Refresh.UnixMilli(),
}
s.logger.Info("[INFO] 成功生成玩家证书", zap.String("uuid", uuid))
return certificate, nil
}
func (s *yggdrasilService) GetPublicKey(ctx context.Context) (string, error) {
return s.signatureService.GetPublicKeyFromRedis()
}
type Property struct {
Name string `json:"name"`
Value string `json:"value"`
Signature string `json:"signature,omitempty"`
}

View File

@@ -0,0 +1,131 @@
package service
import (
"carrotskin/internal/model"
"carrotskin/internal/repository"
"carrotskin/pkg/redis"
"carrotskin/pkg/utils"
"context"
"errors"
"fmt"
"go.uber.org/zap"
"gorm.io/gorm"
)
// yggdrasilServiceComposite 组合服务,保持接口兼容性
// 将认证、会话、序列化、证书服务组合在一起
type yggdrasilServiceComposite struct {
authService *yggdrasilAuthService
sessionService SessionService
serializationService SerializationService
certificateService CertificateService
profileRepo repository.ProfileRepository
tokenRepo repository.TokenRepository
logger *zap.Logger
}
// NewYggdrasilServiceComposite 创建组合服务实例
func NewYggdrasilServiceComposite(
db *gorm.DB,
userRepo repository.UserRepository,
profileRepo repository.ProfileRepository,
tokenRepo repository.TokenRepository,
yggdrasilRepo repository.YggdrasilRepository,
signatureService *signatureService,
redisClient *redis.Client,
logger *zap.Logger,
) YggdrasilService {
// 创建各个专门的服务
authService := NewYggdrasilAuthService(db, userRepo, yggdrasilRepo, logger)
sessionService := NewSessionService(redisClient, logger)
serializationService := NewSerializationService(
repository.NewTextureRepository(db),
signatureService,
logger,
)
certificateService := NewCertificateService(profileRepo, signatureService, logger)
return &yggdrasilServiceComposite{
authService: authService,
sessionService: sessionService,
serializationService: serializationService,
certificateService: certificateService,
profileRepo: profileRepo,
tokenRepo: tokenRepo,
logger: logger,
}
}
// GetUserIDByEmail 获取用户ID通过邮箱
func (s *yggdrasilServiceComposite) GetUserIDByEmail(ctx context.Context, email string) (int64, error) {
return s.authService.GetUserIDByEmail(ctx, email)
}
// VerifyPassword 验证密码
func (s *yggdrasilServiceComposite) VerifyPassword(ctx context.Context, password string, userID int64) error {
return s.authService.VerifyPassword(ctx, password, userID)
}
// ResetYggdrasilPassword 重置Yggdrasil密码
func (s *yggdrasilServiceComposite) ResetYggdrasilPassword(ctx context.Context, userID int64) (string, error) {
return s.authService.ResetYggdrasilPassword(ctx, userID)
}
// JoinServer 加入服务器
func (s *yggdrasilServiceComposite) JoinServer(ctx context.Context, serverID, accessToken, selectedProfile, ip string) error {
// 验证Token
token, err := s.tokenRepo.FindByAccessToken(accessToken)
if err != nil {
s.logger.Error("验证Token失败",
zap.Error(err),
zap.String("accessToken", accessToken),
)
return fmt.Errorf("验证Token失败: %w", err)
}
// 格式化UUID并验证与Token关联的配置文件
formattedProfile := utils.FormatUUID(selectedProfile)
if token.ProfileId != formattedProfile {
return errors.New("selectedProfile与Token不匹配")
}
// 获取Profile以获取用户名
profile, err := s.profileRepo.FindByUUID(formattedProfile)
if err != nil {
s.logger.Error("获取Profile失败",
zap.Error(err),
zap.String("uuid", formattedProfile),
)
return fmt.Errorf("获取Profile失败: %w", err)
}
// 使用会话服务创建会话
return s.sessionService.CreateSession(ctx, serverID, accessToken, profile.Name, formattedProfile, ip)
}
// HasJoinedServer 验证玩家是否已加入服务器
func (s *yggdrasilServiceComposite) HasJoinedServer(ctx context.Context, serverID, username, ip string) error {
return s.sessionService.ValidateSession(ctx, serverID, username, ip)
}
// SerializeProfile 序列化档案
func (s *yggdrasilServiceComposite) SerializeProfile(ctx context.Context, profile model.Profile) map[string]interface{} {
return s.serializationService.SerializeProfile(ctx, profile)
}
// SerializeUser 序列化用户
func (s *yggdrasilServiceComposite) SerializeUser(ctx context.Context, user *model.User, uuid string) map[string]interface{} {
return s.serializationService.SerializeUser(ctx, user, uuid)
}
// GeneratePlayerCertificate 生成玩家证书
func (s *yggdrasilServiceComposite) GeneratePlayerCertificate(ctx context.Context, uuid string) (map[string]interface{}, error) {
return s.certificateService.GeneratePlayerCertificate(ctx, uuid)
}
// GetPublicKey 获取公钥
func (s *yggdrasilServiceComposite) GetPublicKey(ctx context.Context) (string, error) {
return s.certificateService.GetPublicKey(ctx)
}

View File

@@ -0,0 +1,181 @@
package service
import (
apperrors "carrotskin/internal/errors"
"carrotskin/pkg/redis"
"context"
"fmt"
"net"
"strings"
"time"
"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.ErrUsernameMismatch
}
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 获取会话数据
func (s *yggdrasilSessionService) GetSession(ctx context.Context, serverID string) (*SessionData, error) {
if err := ValidateServerID(serverID); err != nil {
return nil, err
}
// 从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
}
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
}

View File

@@ -0,0 +1,81 @@
package service
import (
"errors"
"net"
"regexp"
"strings"
)
// Validator Yggdrasil验证器
type Validator struct{}
// NewValidator 创建验证器实例
func NewValidator() *Validator {
return &Validator{}
}
var (
// emailRegex 邮箱正则表达式
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
)
// ValidateServerID 验证服务器ID格式
func (v *Validator) ValidateServerID(serverID string) error {
if serverID == "" {
return errors.New("服务器ID不能为空")
}
if len(serverID) > 100 {
return errors.New("服务器ID长度超过限制最大100字符")
}
// 防止注入攻击:检查危险字符
if strings.ContainsAny(serverID, "<>\"'&") {
return errors.New("服务器ID包含非法字符")
}
return nil
}
// ValidateIP 验证IP地址格式
func (v *Validator) ValidateIP(ip string) error {
if ip == "" {
return nil // IP是可选的
}
if net.ParseIP(ip) == nil {
return errors.New("IP地址格式无效")
}
return nil
}
// ValidateEmail 验证邮箱格式
func (v *Validator) ValidateEmail(email string) error {
if email == "" {
return errors.New("邮箱不能为空")
}
if !emailRegex.MatchString(email) {
return errors.New("邮箱格式不正确")
}
return nil
}
// ValidateUUID 验证UUID格式简单验证
func (v *Validator) ValidateUUID(uuid string) error {
if uuid == "" {
return errors.New("UUID不能为空")
}
// UUID格式xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (32个十六进制字符 + 4个连字符)
if len(uuid) < 32 || len(uuid) > 36 {
return errors.New("UUID格式无效")
}
return nil
}
// ValidateAccessToken 验证访问令牌
func (v *Validator) ValidateAccessToken(token string) error {
if token == "" {
return errors.New("访问令牌不能为空")
}
if len(token) < 10 {
return errors.New("访问令牌格式无效")
}
return nil
}