refactor: 重构服务层和仓库层
This commit is contained in:
@@ -86,7 +86,8 @@ func NewContainer(
|
||||
|
||||
// 初始化SignatureService
|
||||
signatureService := service.NewSignatureService(c.ProfileRepo, redisClient, logger)
|
||||
c.YggdrasilService = service.NewYggdrasilService(db, c.UserRepo, c.ProfileRepo, c.TextureRepo, c.TokenRepo, c.YggdrasilRepo, signatureService, redisClient, logger)
|
||||
// 使用组合服务(内部包含认证、会话、序列化、证书服务)
|
||||
c.YggdrasilService = service.NewYggdrasilServiceComposite(db, c.UserRepo, c.ProfileRepo, c.TokenRepo, c.YggdrasilRepo, signatureService, redisClient, logger)
|
||||
|
||||
// 初始化其他服务
|
||||
c.SecurityService = service.NewSecurityService(redisClient)
|
||||
|
||||
@@ -47,6 +47,19 @@ var (
|
||||
ErrStorageUnavailable = errors.New("存储服务不可用")
|
||||
ErrUploadFailed = errors.New("上传失败")
|
||||
|
||||
// Yggdrasil相关错误
|
||||
ErrPasswordMismatch = errors.New("密码错误")
|
||||
ErrPasswordNotSet = errors.New("未生成密码")
|
||||
ErrInvalidServerID = errors.New("服务器ID格式无效")
|
||||
ErrSessionNotFound = errors.New("会话不存在或已过期")
|
||||
ErrSessionMismatch = errors.New("会话验证失败")
|
||||
ErrUsernameMismatch = errors.New("用户名不匹配")
|
||||
ErrIPMismatch = errors.New("IP地址不匹配")
|
||||
ErrInvalidAccessToken = errors.New("访问令牌无效")
|
||||
ErrProfileMismatch = errors.New("selectedProfile与Token不匹配")
|
||||
ErrUUIDRequired = errors.New("UUID不能为空")
|
||||
ErrCertificateGenerate = errors.New("生成证书失败")
|
||||
|
||||
// 通用错误
|
||||
ErrBadRequest = errors.New("请求参数错误")
|
||||
ErrInternalServer = errors.New("服务器内部错误")
|
||||
|
||||
227
internal/handler/customskin_handler.go
Normal file
227
internal/handler/customskin_handler.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"carrotskin/internal/container"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CustomSkinHandler CustomSkinAPI处理器
|
||||
type CustomSkinHandler struct {
|
||||
container *container.Container
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCustomSkinHandler 创建CustomSkinHandler实例
|
||||
func NewCustomSkinHandler(c *container.Container) *CustomSkinHandler {
|
||||
return &CustomSkinHandler{
|
||||
container: c,
|
||||
logger: c.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CustomSkinAPIResponse CustomSkinAPI响应格式
|
||||
type CustomSkinAPIResponse struct {
|
||||
Username string `json:"username"`
|
||||
Textures map[string]string `json:"textures,omitempty"`
|
||||
Skin string `json:"skin,omitempty"`
|
||||
Cape string `json:"cape,omitempty"`
|
||||
Elytra string `json:"elytra,omitempty"`
|
||||
}
|
||||
|
||||
// GetPlayerInfo 获取玩家信息
|
||||
// GET {ROOT}/{USERNAME}.json
|
||||
func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
|
||||
username := c.Param("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 移除 .json 后缀(如果存在)
|
||||
username = strings.TrimSuffix(username, ".json")
|
||||
|
||||
// 查找Profile(不区分大小写)
|
||||
profile, err := h.container.ProfileService.GetByProfileName(c.Request.Context(), username)
|
||||
if err != nil {
|
||||
h.logger.Debug("未找到玩家",
|
||||
zap.String("username", username),
|
||||
zap.Error(err),
|
||||
)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "玩家未找到"})
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := CustomSkinAPIResponse{
|
||||
Username: profile.Name,
|
||||
}
|
||||
|
||||
// Profile 已经通过 GetByProfileName 预加载了 Skin 和 Cape
|
||||
|
||||
// 构建材质字典
|
||||
textures := make(map[string]string)
|
||||
hasSkin := false
|
||||
hasCape := false
|
||||
hasElytra := false
|
||||
|
||||
// 处理皮肤
|
||||
if profile.SkinID != nil && profile.Skin != nil {
|
||||
skinHash := profile.Skin.Hash
|
||||
hasSkin = true
|
||||
if profile.Skin.IsSlim {
|
||||
// 如果是slim模型,优先添加到slim,然后添加default
|
||||
textures["slim"] = skinHash
|
||||
textures["default"] = skinHash
|
||||
} else {
|
||||
// 如果是default模型,优先添加到default,然后添加slim
|
||||
textures["default"] = skinHash
|
||||
textures["slim"] = skinHash
|
||||
}
|
||||
}
|
||||
|
||||
// 处理披风
|
||||
if profile.CapeID != nil && profile.Cape != nil {
|
||||
textures["cape"] = profile.Cape.Hash
|
||||
hasCape = true
|
||||
}
|
||||
|
||||
// 处理鞘翅(使用cape的hash,如果存在cape)
|
||||
if hasCape && profile.Cape != nil {
|
||||
textures["elytra"] = profile.Cape.Hash
|
||||
hasElytra = true
|
||||
}
|
||||
|
||||
// 根据材质字典决定返回格式
|
||||
// 根据协议,如果只有皮肤(使用default模型),可以使用缩略格式
|
||||
// 但如果有多个不同的材质或需要指定模型,使用完整格式
|
||||
if hasSkin && !hasCape && !hasElytra {
|
||||
// 如果只有皮肤,使用缩略格式(使用default模型的hash)
|
||||
if defaultHash, exists := textures["default"]; exists {
|
||||
response.Skin = defaultHash
|
||||
} else if slimHash, exists := textures["slim"]; exists {
|
||||
// 如果只有slim,也使用缩略格式(但协议说这会导致手臂渲染错误)
|
||||
response.Skin = slimHash
|
||||
}
|
||||
} else if len(textures) > 0 {
|
||||
// 如果有多个材质或需要指定模型,使用完整格式
|
||||
response.Textures = textures
|
||||
}
|
||||
// 如果没有材质,不设置 textures 和 skin 字段(留空)
|
||||
|
||||
// 设置缓存头
|
||||
c.Header("Cache-Control", "public, max-age=300") // 5分钟缓存
|
||||
c.Header("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// 响应If-Modified-Since
|
||||
if modifiedSince := c.GetHeader("If-Modified-Since"); modifiedSince != "" {
|
||||
if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil {
|
||||
// 如果资源未修改,返回304
|
||||
if profile.UpdatedAt.Before(t.Add(time.Second)) {
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置Last-Modified
|
||||
c.Header("Last-Modified", profile.UpdatedAt.UTC().Format(http.TimeFormat))
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetTexture 获取资源文件
|
||||
// GET {ROOT}/textures/{hash}
|
||||
func (h *CustomSkinHandler) GetTexture(c *gin.Context) {
|
||||
hash := c.Param("hash")
|
||||
if hash == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "资源标识符不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 查找Texture
|
||||
texture, err := h.container.TextureService.GetByHash(c.Request.Context(), hash)
|
||||
if err != nil {
|
||||
h.logger.Debug("未找到材质",
|
||||
zap.String("hash", hash),
|
||||
zap.Error(err),
|
||||
)
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "资源未找到"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查材质状态
|
||||
if texture.Status != 1 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "资源不可用"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析文件URL获取bucket和objectName
|
||||
if h.container.Storage == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储服务不可用"})
|
||||
return
|
||||
}
|
||||
|
||||
bucket, objectName, err := h.container.Storage.ParseFileURL(texture.URL)
|
||||
if err != nil {
|
||||
h.logger.Error("解析文件URL失败",
|
||||
zap.String("url", texture.URL),
|
||||
zap.Error(err),
|
||||
)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "解析文件URL失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件对象
|
||||
ctx := c.Request.Context()
|
||||
reader, objInfo, err := h.container.Storage.GetObject(ctx, bucket, objectName)
|
||||
if err != nil {
|
||||
h.logger.Error("获取文件失败",
|
||||
zap.String("bucket", bucket),
|
||||
zap.String("objectName", objectName),
|
||||
zap.Error(err),
|
||||
)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取文件失败"})
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// 设置HTTP头
|
||||
c.Header("Content-Type", objInfo.ContentType)
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", objInfo.Size))
|
||||
c.Header("Last-Modified", objInfo.LastModified.UTC().Format(http.TimeFormat))
|
||||
c.Header("ETag", objInfo.ETag)
|
||||
c.Header("Cache-Control", "public, max-age=86400") // 24小时缓存
|
||||
|
||||
// 响应If-Modified-Since
|
||||
if modifiedSince := c.GetHeader("If-Modified-Since"); modifiedSince != "" {
|
||||
if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil {
|
||||
// 如果资源未修改,返回304
|
||||
if objInfo.LastModified.Before(t.Add(time.Second)) {
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应If-None-Match (ETag)
|
||||
if noneMatch := c.GetHeader("If-None-Match"); noneMatch != "" {
|
||||
if noneMatch == objInfo.ETag || noneMatch == fmt.Sprintf(`"%s"`, objInfo.ETag) {
|
||||
c.Status(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 增加下载计数(异步)
|
||||
go func() {
|
||||
_ = h.container.TextureRepo.IncrementDownloadCount(texture.ID)
|
||||
}()
|
||||
|
||||
// 流式传输文件内容
|
||||
c.DataFromReader(http.StatusOK, objInfo.Size, objInfo.ContentType, reader, nil)
|
||||
}
|
||||
@@ -10,30 +10,32 @@ import (
|
||||
|
||||
// Handlers 集中管理所有Handler
|
||||
type Handlers struct {
|
||||
Auth *AuthHandler
|
||||
User *UserHandler
|
||||
Texture *TextureHandler
|
||||
Profile *ProfileHandler
|
||||
Captcha *CaptchaHandler
|
||||
Yggdrasil *YggdrasilHandler
|
||||
Auth *AuthHandler
|
||||
User *UserHandler
|
||||
Texture *TextureHandler
|
||||
Profile *ProfileHandler
|
||||
Captcha *CaptchaHandler
|
||||
Yggdrasil *YggdrasilHandler
|
||||
CustomSkin *CustomSkinHandler
|
||||
}
|
||||
|
||||
// NewHandlers 创建所有Handler实例
|
||||
func NewHandlers(c *container.Container) *Handlers {
|
||||
return &Handlers{
|
||||
Auth: NewAuthHandler(c),
|
||||
User: NewUserHandler(c),
|
||||
Texture: NewTextureHandler(c),
|
||||
Profile: NewProfileHandler(c),
|
||||
Captcha: NewCaptchaHandler(c),
|
||||
Yggdrasil: NewYggdrasilHandler(c),
|
||||
Auth: NewAuthHandler(c),
|
||||
User: NewUserHandler(c),
|
||||
Texture: NewTextureHandler(c),
|
||||
Profile: NewProfileHandler(c),
|
||||
Captcha: NewCaptchaHandler(c),
|
||||
Yggdrasil: NewYggdrasilHandler(c),
|
||||
CustomSkin: NewCustomSkinHandler(c),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutesWithDI 使用依赖注入注册所有路由
|
||||
func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||
// 设置Swagger文档
|
||||
SetupSwagger(router)
|
||||
// 健康检查路由
|
||||
router.GET("/health", HealthCheck)
|
||||
|
||||
// 创建Handler实例
|
||||
h := NewHandlers(c)
|
||||
@@ -61,6 +63,9 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||
|
||||
// 系统路由
|
||||
registerSystemRoutes(v1)
|
||||
|
||||
// CustomSkinAPI 路由
|
||||
registerCustomSkinRoutes(v1, h.CustomSkin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,3 +196,22 @@ func registerSystemRoutes(v1 *gin.RouterGroup) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// registerCustomSkinRoutes 注册CustomSkinAPI路由
|
||||
// CustomSkinAPI 协议要求根地址必须以 / 结尾
|
||||
// 路由格式:
|
||||
// - {ROOT}/{USERNAME}.json - 获取玩家信息
|
||||
// - {ROOT}/textures/{hash} - 获取资源文件
|
||||
//
|
||||
// 根路径为 /api/v1/csl/
|
||||
func registerCustomSkinRoutes(v1 *gin.RouterGroup, h *CustomSkinHandler) {
|
||||
// CustomSkinAPI 路由组
|
||||
csl := v1.Group("/csl")
|
||||
{
|
||||
// 获取玩家信息: {ROOT}/{USERNAME}.json
|
||||
csl.GET("/:username", h.GetPlayerInfo)
|
||||
|
||||
// 获取资源文件: {ROOT}/textures/{hash}
|
||||
csl.GET("/textures/:hash", h.GetTexture)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,56 +4,9 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// @title CarrotSkin API
|
||||
// @version 1.0
|
||||
// @description CarrotSkin 是一个优秀的 Minecraft 皮肤站 API 服务
|
||||
// @description
|
||||
// @description ## 功能特性
|
||||
// @description - 用户注册/登录/管理
|
||||
// @description - 材质上传/下载/管理
|
||||
// @description - Minecraft 档案管理
|
||||
// @description - 权限控制系统
|
||||
// @description - 积分系统
|
||||
// @description
|
||||
// @description ## 认证方式
|
||||
// @description 使用 JWT Token 进行身份认证,需要在请求头中包含:
|
||||
// @description ```
|
||||
// @description Authorization: Bearer <your-jwt-token>
|
||||
// @description ```
|
||||
|
||||
// @contact.name CarrotSkin Team
|
||||
// @contact.email support@carrotskin.com
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /api/v1
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
|
||||
func SetupSwagger(router *gin.Engine) {
|
||||
// Swagger文档路由
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// 健康检查接口
|
||||
router.GET("/health", HealthCheck)
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
// @Summary 健康检查
|
||||
// @Description 检查服务是否正常运行
|
||||
// @Tags system
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "成功"
|
||||
// @Router /health [get]
|
||||
func HealthCheck(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
|
||||
@@ -83,3 +83,5 @@ type YggdrasilRepository interface {
|
||||
ResetPassword(id int64, password string) error
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,11 @@ func (r *profileRepository) FindByUUID(uuid string) (*model.Profile, error) {
|
||||
|
||||
func (r *profileRepository) FindByName(name string) (*model.Profile, error) {
|
||||
var profile model.Profile
|
||||
err := r.db.Where("name = ?", name).First(&profile).Error
|
||||
// 使用 LOWER 函数进行不区分大小写的查询,并预加载 Skin 和 Cape
|
||||
err := r.db.Where("LOWER(name) = LOWER(?)", name).
|
||||
Preload("Skin").
|
||||
Preload("Cape").
|
||||
First(&profile).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,7 +102,10 @@ func (r *profileRepository) UpdateLastUsedAt(uuid string) error {
|
||||
|
||||
func (r *profileRepository) GetByNames(names []string) ([]*model.Profile, error) {
|
||||
var profiles []*model.Profile
|
||||
err := r.db.Where("name in (?)", names).Find(&profiles).Error
|
||||
err := r.db.Where("name in (?)", names).
|
||||
Preload("Skin").
|
||||
Preload("Cape").
|
||||
Find(&profiles).Error
|
||||
return profiles, err
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func (r *tokenRepository) GetByUserID(userId int64) ([]*model.Token, error) {
|
||||
|
||||
func (r *tokenRepository) GetUUIDByAccessToken(accessToken string) (string, error) {
|
||||
var token model.Token
|
||||
err := r.db.Where("access_token = ?", accessToken).First(&token).Error
|
||||
err := r.db.Select("profile_id").Where("access_token = ?", accessToken).First(&token).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (r *tokenRepository) GetUUIDByAccessToken(accessToken string) (string, erro
|
||||
|
||||
func (r *tokenRepository) GetUserIDByAccessToken(accessToken string) (int64, error) {
|
||||
var token model.Token
|
||||
err := r.db.Where("access_token = ?", accessToken).First(&token).Error
|
||||
err := r.db.Select("user_id").Where("access_token = ?", accessToken).First(&token).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func NewYggdrasilRepository(db *gorm.DB) YggdrasilRepository {
|
||||
|
||||
func (r *yggdrasilRepository) GetPasswordByID(id int64) (string, error) {
|
||||
var yggdrasil model.Yggdrasil
|
||||
err := r.db.Where("id = ?", id).First(&yggdrasil).Error
|
||||
err := r.db.Select("password").Where("id = ?", id).First(&yggdrasil).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -28,5 +28,3 @@ func (r *yggdrasilRepository) GetPasswordByID(id int64) (string, error) {
|
||||
func (r *yggdrasilRepository) ResetPassword(id int64, password string) error {
|
||||
return r.db.Model(&model.Yggdrasil{}).Where("id = ?", id).Update("password", password).Error
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
91
internal/service/yggdrasil_auth_service.go
Normal file
91
internal/service/yggdrasil_auth_service.go
Normal 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
|
||||
}
|
||||
112
internal/service/yggdrasil_certificate_service.go
Normal file
112
internal/service/yggdrasil_certificate_service.go
Normal 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()
|
||||
}
|
||||
|
||||
156
internal/service/yggdrasil_serialization_service.go
Normal file
156
internal/service/yggdrasil_serialization_service.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
131
internal/service/yggdrasil_service_composite.go
Normal file
131
internal/service/yggdrasil_service_composite.go
Normal 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)
|
||||
}
|
||||
181
internal/service/yggdrasil_session_service.go
Normal file
181
internal/service/yggdrasil_session_service.go
Normal 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
|
||||
}
|
||||
81
internal/service/yggdrasil_validator.go
Normal file
81
internal/service/yggdrasil_validator.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user