Files
backend/internal/handler/yggdrasil_handler.go
lafay 801f1b1397 refactor: Implement dependency injection for handlers and services
- Refactored AuthHandler, UserHandler, TextureHandler, ProfileHandler, CaptchaHandler, and YggdrasilHandler to use dependency injection.
- Removed direct instantiation of services and repositories within handlers, replacing them with constructor injection.
- Updated the container to initialize service instances and provide them to handlers.
- Enhanced code structure for better testability and adherence to Go best practices.
2025-12-02 19:47:04 +08:00

595 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"bytes"
"carrotskin/internal/container"
"carrotskin/internal/model"
"carrotskin/internal/service"
"carrotskin/pkg/utils"
"io"
"net/http"
"regexp"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// 常量定义
const (
ErrInternalServer = "服务器内部错误"
// 错误类型
ErrInvalidEmailFormat = "邮箱格式不正确"
ErrInvalidPassword = "密码必须至少包含8个字符只能包含字母、数字和特殊字符"
ErrWrongPassword = "密码错误"
ErrUserNotMatch = "用户不匹配"
// 错误消息
ErrInvalidRequest = "请求格式无效"
ErrJoinServerFailed = "加入服务器失败"
ErrServerIDRequired = "服务器ID不能为空"
ErrUsernameRequired = "用户名不能为空"
ErrSessionVerifyFailed = "会话验证失败"
ErrProfileNotFound = "未找到用户配置文件"
ErrInvalidParams = "无效的请求参数"
ErrEmptyUserID = "用户ID为空"
ErrUnauthorized = "无权操作此配置文件"
ErrGetProfileService = "获取配置文件服务失败"
// 成功信息
SuccessProfileCreated = "创建成功"
MsgRegisterSuccess = "注册成功"
// 错误消息
ErrGetProfile = "获取配置文件失败"
ErrGetTextureService = "获取材质服务失败"
ErrInvalidContentType = "无效的请求内容类型"
ErrParseMultipartForm = "解析多部分表单失败"
ErrGetFileFromForm = "从表单获取文件失败"
ErrInvalidFileType = "无效的文件类型仅支持PNG图片"
ErrSaveTexture = "保存材质失败"
ErrSetTexture = "设置材质失败"
ErrGetTexture = "获取材质失败"
// 内存限制
MaxMultipartMemory = 32 << 20 // 32 MB
// 材质类型
TextureTypeSkin = "SKIN"
TextureTypeCape = "CAPE"
// 内容类型
ContentTypePNG = "image/png"
ContentTypeMultipart = "multipart/form-data"
// 表单参数
FormKeyModel = "model"
FormKeyFile = "file"
// 元数据键
MetaKeyModel = "model"
)
// 正则表达式
var (
// 邮箱正则表达式
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
// 密码强度正则表达式最少8位只允许字母、数字和特定特殊字符
passwordRegex = regexp.MustCompile(`^[a-zA-Z0-9!@#$%^&*]{8,}$`)
)
// 请求结构体
type (
// AuthenticateRequest 认证请求
AuthenticateRequest struct {
Agent map[string]interface{} `json:"agent"`
ClientToken string `json:"clientToken"`
Identifier string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
RequestUser bool `json:"requestUser"`
}
// ValidTokenRequest 验证令牌请求
ValidTokenRequest struct {
AccessToken string `json:"accessToken" binding:"required"`
ClientToken string `json:"clientToken"`
}
// RefreshRequest 刷新令牌请求
RefreshRequest struct {
AccessToken string `json:"accessToken" binding:"required"`
ClientToken string `json:"clientToken"`
RequestUser bool `json:"requestUser"`
SelectedProfile map[string]interface{} `json:"selectedProfile"`
}
// SignOutRequest 登出请求
SignOutRequest struct {
Email string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// JoinServerRequest 加入服务器请求
JoinServerRequest struct {
ServerID string `json:"serverId" binding:"required"`
AccessToken string `json:"accessToken" binding:"required"`
SelectedProfile string `json:"selectedProfile" binding:"required"`
}
)
// 响应结构体
type (
// AuthenticateResponse 认证响应
AuthenticateResponse struct {
AccessToken string `json:"accessToken"`
ClientToken string `json:"clientToken"`
SelectedProfile map[string]interface{} `json:"selectedProfile,omitempty"`
AvailableProfiles []map[string]interface{} `json:"availableProfiles"`
User map[string]interface{} `json:"user,omitempty"`
}
// RefreshResponse 刷新令牌响应
RefreshResponse struct {
AccessToken string `json:"accessToken"`
ClientToken string `json:"clientToken"`
SelectedProfile map[string]interface{} `json:"selectedProfile,omitempty"`
User map[string]interface{} `json:"user,omitempty"`
}
)
// APIResponse API响应
type APIResponse struct {
Status int `json:"status"`
Data interface{} `json:"data"`
Error interface{} `json:"error"`
}
// standardResponse 生成标准响应
func standardResponse(c *gin.Context, status int, data interface{}, err interface{}) {
c.JSON(status, APIResponse{
Status: status,
Data: data,
Error: err,
})
}
// YggdrasilHandler Yggdrasil API处理器
type YggdrasilHandler struct {
container *container.Container
logger *zap.Logger
}
// NewYggdrasilHandler 创建YggdrasilHandler实例
func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler {
return &YggdrasilHandler{
container: c,
logger: c.Logger,
}
}
// Authenticate 用户认证
func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
rawData, err := io.ReadAll(c.Request.Body)
if err != nil {
h.logger.Error("读取请求体失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"})
return
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(rawData))
var request AuthenticateRequest
if err = c.ShouldBindJSON(&request); err != nil {
h.logger.Error("解析认证请求失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var userId int64
var profile *model.Profile
var UUID string
if emailRegex.MatchString(request.Identifier) {
userId, err = service.GetUserIDByEmail(h.container.DB, request.Identifier)
} else {
profile, err = service.GetProfileByProfileName(h.container.DB, request.Identifier)
if err != nil {
h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err))
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
userId = profile.UserID
UUID = profile.UUID
}
if err != nil {
h.logger.Warn("认证失败: 用户不存在", zap.String("identifier", request.Identifier), zap.Error(err))
c.JSON(http.StatusForbidden, gin.H{"error": "用户不存在"})
return
}
if err := service.VerifyPassword(h.container.DB, request.Password, userId); err != nil {
h.logger.Warn("认证失败: 密码错误", zap.Error(err))
c.JSON(http.StatusForbidden, gin.H{"error": ErrWrongPassword})
return
}
selectedProfile, availableProfiles, accessToken, clientToken, err := h.container.TokenService.Create(userId, UUID, request.ClientToken)
if err != nil {
h.logger.Error("生成令牌失败", zap.Error(err), zap.Int64("userId", userId))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
user, err := h.container.UserService.GetByID(userId)
if err != nil {
h.logger.Error("获取用户信息失败", zap.Error(err), zap.Int64("userId", userId))
}
availableProfilesData := make([]map[string]interface{}, 0, len(availableProfiles))
for _, p := range availableProfiles {
availableProfilesData = append(availableProfilesData, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *p))
}
response := AuthenticateResponse{
AccessToken: accessToken,
ClientToken: clientToken,
AvailableProfiles: availableProfilesData,
}
if selectedProfile != nil {
response.SelectedProfile = service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *selectedProfile)
}
if request.RequestUser && user != nil {
response.User = service.SerializeUser(h.logger, user, UUID)
}
h.logger.Info("用户认证成功", zap.Int64("userId", userId))
c.JSON(http.StatusOK, response)
}
// ValidToken 验证令牌
func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
var request ValidTokenRequest
if err := c.ShouldBindJSON(&request); err != nil {
h.logger.Error("解析验证令牌请求失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if h.container.TokenService.Validate(request.AccessToken, request.ClientToken) {
h.logger.Info("令牌验证成功", zap.String("accessToken", request.AccessToken))
c.JSON(http.StatusNoContent, gin.H{"valid": true})
} else {
h.logger.Warn("令牌验证失败", zap.String("accessToken", request.AccessToken))
c.JSON(http.StatusForbidden, gin.H{"valid": false})
}
}
// RefreshToken 刷新令牌
func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
var request RefreshRequest
if err := c.ShouldBindJSON(&request); err != nil {
h.logger.Error("解析刷新令牌请求失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
UUID, err := h.container.TokenService.GetUUIDByAccessToken(request.AccessToken)
if err != nil {
h.logger.Warn("刷新令牌失败: 无效的访问令牌", zap.String("token", request.AccessToken), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID, _ := h.container.TokenService.GetUserIDByAccessToken(request.AccessToken)
UUID = utils.FormatUUID(UUID)
profile, err := h.container.ProfileService.GetByUUID(UUID)
if err != nil {
h.logger.Error("刷新令牌失败: 无法获取用户信息", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var profileData map[string]interface{}
var userData map[string]interface{}
var profileID string
if request.SelectedProfile != nil {
profileIDValue, ok := request.SelectedProfile["id"]
if !ok {
h.logger.Error("刷新令牌失败: 缺少配置文件ID", zap.Int64("userId", userID))
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少配置文件ID"})
return
}
profileID, ok = profileIDValue.(string)
if !ok {
h.logger.Error("刷新令牌失败: 配置文件ID类型错误", zap.Int64("userId", userID))
c.JSON(http.StatusBadRequest, gin.H{"error": "配置文件ID必须是字符串"})
return
}
profileID = utils.FormatUUID(profileID)
if profile.UserID != userID {
h.logger.Warn("刷新令牌失败: 用户不匹配",
zap.Int64("userId", userID),
zap.Int64("profileUserId", profile.UserID),
)
c.JSON(http.StatusBadRequest, gin.H{"error": ErrUserNotMatch})
return
}
profileData = service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile)
}
user, _ := h.container.UserService.GetByID(userID)
if request.RequestUser && user != nil {
userData = service.SerializeUser(h.logger, user, UUID)
}
newAccessToken, newClientToken, err := h.container.TokenService.Refresh(
request.AccessToken,
request.ClientToken,
profileID,
)
if err != nil {
h.logger.Error("刷新令牌失败", zap.Error(err), zap.Int64("userId", userID))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.logger.Info("刷新令牌成功", zap.Int64("userId", userID))
c.JSON(http.StatusOK, RefreshResponse{
AccessToken: newAccessToken,
ClientToken: newClientToken,
SelectedProfile: profileData,
User: userData,
})
}
// InvalidToken 使令牌失效
func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
var request ValidTokenRequest
if err := c.ShouldBindJSON(&request); err != nil {
h.logger.Error("解析使令牌失效请求失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.container.TokenService.Invalidate(request.AccessToken)
h.logger.Info("令牌已失效", zap.String("token", request.AccessToken))
c.JSON(http.StatusNoContent, gin.H{})
}
// SignOut 用户登出
func (h *YggdrasilHandler) SignOut(c *gin.Context) {
var request SignOutRequest
if err := c.ShouldBindJSON(&request); err != nil {
h.logger.Error("解析登出请求失败", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if !emailRegex.MatchString(request.Email) {
h.logger.Warn("登出失败: 邮箱格式不正确", zap.String("email", request.Email))
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidEmailFormat})
return
}
user, err := h.container.UserService.GetByEmail(request.Email)
if err != nil || user == nil {
h.logger.Warn("登出失败: 用户不存在", zap.String("email", request.Email), zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "用户不存在"})
return
}
if err := service.VerifyPassword(h.container.DB, request.Password, user.ID); err != nil {
h.logger.Warn("登出失败: 密码错误", zap.Int64("userId", user.ID))
c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword})
return
}
h.container.TokenService.InvalidateUserTokens(user.ID)
h.logger.Info("用户登出成功", zap.Int64("userId", user.ID))
c.JSON(http.StatusNoContent, gin.H{"valid": true})
}
// GetProfileByUUID 根据UUID获取档案
func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
uuid := utils.FormatUUID(c.Param("uuid"))
h.logger.Info("获取配置文件请求", zap.String("uuid", uuid))
profile, err := h.container.ProfileService.GetByUUID(uuid)
if err != nil {
h.logger.Error("获取配置文件失败", zap.Error(err), zap.String("uuid", uuid))
standardResponse(c, http.StatusInternalServerError, nil, err.Error())
return
}
h.logger.Info("成功获取配置文件", zap.String("uuid", uuid), zap.String("name", profile.Name))
c.JSON(http.StatusOK, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile))
}
// JoinServer 加入服务器
func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
var request JoinServerRequest
clientIP := c.ClientIP()
if err := c.ShouldBindJSON(&request); err != nil {
h.logger.Error("解析加入服务器请求失败", zap.Error(err), zap.String("ip", clientIP))
standardResponse(c, http.StatusBadRequest, nil, ErrInvalidRequest)
return
}
h.logger.Info("收到加入服务器请求",
zap.String("serverId", request.ServerID),
zap.String("userUUID", request.SelectedProfile),
zap.String("ip", clientIP),
)
if err := service.JoinServer(h.container.DB, h.logger, h.container.Redis, request.ServerID, request.AccessToken, request.SelectedProfile, clientIP); err != nil {
h.logger.Error("加入服务器失败",
zap.Error(err),
zap.String("serverId", request.ServerID),
zap.String("userUUID", request.SelectedProfile),
zap.String("ip", clientIP),
)
standardResponse(c, http.StatusInternalServerError, nil, ErrJoinServerFailed)
return
}
h.logger.Info("加入服务器成功",
zap.String("serverId", request.ServerID),
zap.String("userUUID", request.SelectedProfile),
zap.String("ip", clientIP),
)
c.Status(http.StatusNoContent)
}
// HasJoinedServer 验证玩家是否已加入服务器
func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
clientIP, _ := c.GetQuery("ip")
serverID, exists := c.GetQuery("serverId")
if !exists || serverID == "" {
h.logger.Warn("缺少服务器ID参数", zap.String("ip", clientIP))
standardResponse(c, http.StatusNoContent, nil, ErrServerIDRequired)
return
}
username, exists := c.GetQuery("username")
if !exists || username == "" {
h.logger.Warn("缺少用户名参数", zap.String("serverId", serverID), zap.String("ip", clientIP))
standardResponse(c, http.StatusNoContent, nil, ErrUsernameRequired)
return
}
h.logger.Info("收到会话验证请求",
zap.String("serverId", serverID),
zap.String("username", username),
zap.String("ip", clientIP),
)
if err := service.HasJoinedServer(h.logger, h.container.Redis, serverID, username, clientIP); err != nil {
h.logger.Warn("会话验证失败",
zap.Error(err),
zap.String("serverId", serverID),
zap.String("username", username),
zap.String("ip", clientIP),
)
standardResponse(c, http.StatusNoContent, nil, ErrSessionVerifyFailed)
return
}
profile, err := h.container.ProfileService.GetByUUID(username)
if err != nil {
h.logger.Error("获取用户配置文件失败", zap.Error(err), zap.String("username", username))
standardResponse(c, http.StatusNoContent, nil, ErrProfileNotFound)
return
}
h.logger.Info("会话验证成功",
zap.String("serverId", serverID),
zap.String("username", username),
zap.String("uuid", profile.UUID),
)
c.JSON(200, service.SerializeProfile(h.container.DB, h.logger, h.container.Redis, *profile))
}
// GetProfilesByName 批量获取配置文件
func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
var names []string
if err := c.ShouldBindJSON(&names); err != nil {
h.logger.Error("解析名称数组请求失败", zap.Error(err))
standardResponse(c, http.StatusBadRequest, nil, ErrInvalidParams)
return
}
h.logger.Info("接收到批量获取配置文件请求", zap.Int("count", len(names)))
profiles, err := h.container.ProfileService.GetByNames(names)
if err != nil {
h.logger.Error("获取配置文件失败", zap.Error(err))
}
h.logger.Info("成功获取配置文件", zap.Int("requested", len(names)), zap.Int("returned", len(profiles)))
c.JSON(http.StatusOK, profiles)
}
// GetMetaData 获取Yggdrasil元数据
func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
meta := gin.H{
"implementationName": "CellAuth",
"implementationVersion": "0.0.1",
"serverName": "LittleLan's Yggdrasil Server Implementation.",
"links": gin.H{
"homepage": "https://skin.littlelan.cn",
"register": "https://skin.littlelan.cn/auth",
},
"feature.non_email_login": true,
"feature.enable_profile_key": true,
}
skinDomains := []string{".hitwh.games", ".littlelan.cn"}
signature, err := service.GetPublicKeyFromRedisFunc(h.logger, h.container.Redis)
if err != nil {
h.logger.Error("获取公钥失败", zap.Error(err))
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
return
}
h.logger.Info("提供元数据")
c.JSON(http.StatusOK, gin.H{
"meta": meta,
"skinDomains": skinDomains,
"signaturePublickey": signature,
})
}
// GetPlayerCertificates 获取玩家证书
func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header not provided"})
c.Abort()
return
}
bearerPrefix := "Bearer "
if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
c.Abort()
return
}
tokenID := authHeader[len(bearerPrefix):]
if tokenID == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
c.Abort()
return
}
uuid, err := h.container.TokenService.GetUUIDByAccessToken(tokenID)
if uuid == "" {
h.logger.Error("获取玩家UUID失败", zap.Error(err))
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
return
}
uuid = utils.FormatUUID(uuid)
certificate, err := service.GeneratePlayerCertificate(h.container.DB, h.logger, h.container.Redis, uuid)
if err != nil {
h.logger.Error("生成玩家证书失败", zap.Error(err))
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
return
}
h.logger.Info("成功生成玩家证书")
c.JSON(http.StatusOK, certificate)
}