package handler import ( "bytes" "carrotskin/internal/model" "carrotskin/internal/service" "carrotskin/pkg/database" "carrotskin/pkg/logger" "carrotskin/pkg/redis" "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 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"` } ) 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, }) } // Authenticate 用户认证 func Authenticate(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() // 读取并保存原始请求体,以便多次读取 rawData, err := io.ReadAll(c.Request.Body) if err != nil { loggerInstance.Error("[ERROR] 读取请求体失败: ", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"}) return } c.Request.Body = io.NopCloser(bytes.NewBuffer(rawData)) // 绑定JSON数据到请求结构体 var request AuthenticateRequest if err = c.ShouldBindJSON(&request); err != nil { loggerInstance.Error("[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(db, request.Identifier) } else { profile, err = service.GetProfileByProfileName(db, request.Identifier) if err != nil { loggerInstance.Error("[ERROR] 用户名不存在: ", zap.String("标识符", request.Identifier), zap.Error(err)) c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } userId = profile.UserID UUID = profile.UUID } if err != nil { loggerInstance.Warn("[WARN] 认证失败: 用户不存在", zap.String("标识符:", request.Identifier), zap.Error(err)) return } // 验证密码 err = service.VerifyPassword(db, request.Password, userId) if err != nil { loggerInstance.Warn("[WARN] 认证失败:", zap.Error(err)) c.JSON(http.StatusForbidden, gin.H{"error": ErrWrongPassword}) return } // 生成新令牌 selectedProfile, availableProfiles, accessToken, clientToken, err := service.NewToken(db, loggerInstance, userId, UUID, request.ClientToken) if err != nil { loggerInstance.Error("[ERROR] 生成令牌失败:", zap.Error(err), zap.Any("用户ID:", userId)) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } user, err := service.GetUserByID(userId) if err != nil { loggerInstance.Error("[ERROR] id查找错误:", zap.Error(err), zap.Any("ID:", userId)) } // 处理可用的配置文件 redisClient := redis.MustGetClient() availableProfilesData := make([]map[string]interface{}, 0, len(availableProfiles)) for _, profile := range availableProfiles { availableProfilesData = append(availableProfilesData, service.SerializeProfile(db, loggerInstance, redisClient, *profile)) } response := AuthenticateResponse{ AccessToken: accessToken, ClientToken: clientToken, AvailableProfiles: availableProfilesData, } if selectedProfile != nil { response.SelectedProfile = service.SerializeProfile(db, loggerInstance, redisClient, *selectedProfile) } if request.RequestUser { // 使用 SerializeUser 来正确处理 Properties 字段 response.User = service.SerializeUser(loggerInstance, user, UUID) } // 返回认证响应 loggerInstance.Info("[INFO] 用户认证成功", zap.Any("用户ID:", userId)) c.JSON(http.StatusOK, response) } // ValidToken 验证令牌 func ValidToken(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() var request ValidTokenRequest if err := c.ShouldBindJSON(&request); err != nil { loggerInstance.Error("[ERROR] 解析验证令牌请求失败: ", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 验证令牌 if service.ValidToken(db, request.AccessToken, request.ClientToken) { loggerInstance.Info("[INFO] 令牌验证成功", zap.Any("访问令牌:", request.AccessToken)) c.JSON(http.StatusNoContent, gin.H{"valid": true}) } else { loggerInstance.Warn("[WARN] 令牌验证失败", zap.Any("访问令牌:", request.AccessToken)) c.JSON(http.StatusForbidden, gin.H{"valid": false}) } } // RefreshToken 刷新令牌 func RefreshToken(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() var request RefreshRequest if err := c.ShouldBindJSON(&request); err != nil { loggerInstance.Error("[ERROR] 解析刷新令牌请求失败: ", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 获取用户ID和用户信息 UUID, err := service.GetUUIDByAccessToken(db, request.AccessToken) if err != nil { loggerInstance.Warn("[WARN] 刷新令牌失败: 无效的访问令牌", zap.Any("令牌:", request.AccessToken), zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID, _ := service.GetUserIDByAccessToken(db, request.AccessToken) // 格式化UUID 这里是因为HMCL的传入参数是HEX格式,为了兼容HMCL,在此做处理 UUID = utils.FormatUUID(UUID) profile, err := service.GetProfileByUUID(db, UUID) if err != nil { loggerInstance.Error("[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 { // 验证profileID是否存在 profileIDValue, ok := request.SelectedProfile["id"] if !ok { loggerInstance.Error("[ERROR] 刷新令牌失败: 缺少配置文件ID", zap.Any("ID:", userID)) c.JSON(http.StatusBadRequest, gin.H{"error": "缺少配置文件ID"}) return } // 类型断言 profileID, ok = profileIDValue.(string) if !ok { loggerInstance.Error("[ERROR] 刷新令牌失败: 配置文件ID类型错误 ", zap.Any("用户ID:", userID)) c.JSON(http.StatusBadRequest, gin.H{"error": "配置文件ID必须是字符串"}) return } // 格式化profileID profileID = utils.FormatUUID(profileID) // 验证配置文件所属用户 if profile.UserID != userID { loggerInstance.Warn("[WARN] 刷新令牌失败: 用户不匹配 ", zap.Any("用户ID:", userID), zap.Any("配置文件用户ID:", profile.UserID)) c.JSON(http.StatusBadRequest, gin.H{"error": ErrUserNotMatch}) return } profileData = service.SerializeProfile(db, loggerInstance, redis.MustGetClient(), *profile) } user, _ := service.GetUserByID(userID) // 添加用户信息(如果请求了) if request.RequestUser { userData = service.SerializeUser(loggerInstance, user, UUID) } // 刷新令牌 newAccessToken, newClientToken, err := service.RefreshToken(db, loggerInstance, request.AccessToken, request.ClientToken, profileID, ) if err != nil { loggerInstance := logger.MustGetLogger() loggerInstance.Error("[ERROR] 刷新令牌失败: ", zap.Error(err), zap.Any("用户ID: ", userID)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 返回响应 loggerInstance.Info("[INFO] 刷新令牌成功", zap.Any("用户ID:", userID)) c.JSON(http.StatusOK, RefreshResponse{ AccessToken: newAccessToken, ClientToken: newClientToken, SelectedProfile: profileData, User: userData, }) } // InvalidToken 使令牌失效 func InvalidToken(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() var request ValidTokenRequest if err := c.ShouldBindJSON(&request); err != nil { loggerInstance.Error("[ERROR] 解析使令牌失效请求失败: ", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 使令牌失效 service.InvalidToken(db, loggerInstance, request.AccessToken) loggerInstance.Info("[INFO] 令牌已使失效", zap.Any("访问令牌:", request.AccessToken)) c.JSON(http.StatusNoContent, gin.H{}) } // SignOut 用户登出 func SignOut(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() var request SignOutRequest if err := c.ShouldBindJSON(&request); err != nil { loggerInstance.Error("[ERROR] 解析登出请求失败: %v", zap.Error(err)) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 验证邮箱格式 if !emailRegex.MatchString(request.Email) { loggerInstance.Warn("[WARN] 登出失败: 邮箱格式不正确 ", zap.Any(" ", request.Email)) c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidEmailFormat}) return } // 通过邮箱获取用户 user, err := service.GetUserByEmail(request.Email) if err != nil { loggerInstance.Warn( "登出失败: 用户不存在", zap.String("邮箱", request.Email), zap.Error(err), ) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 验证密码 if err := service.VerifyPassword(db, request.Password, user.ID); err != nil { loggerInstance.Warn("[WARN] 登出失败: 密码错误", zap.Any("用户ID:", user.ID)) c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword}) return } // 使该用户的所有令牌失效 service.InvalidUserTokens(db, loggerInstance, user.ID) loggerInstance.Info("[INFO] 用户登出成功", zap.Any("用户ID:", user.ID)) c.JSON(http.StatusNoContent, gin.H{"valid": true}) } func GetProfileByUUID(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() redisClient := redis.MustGetClient() // 获取并格式化UUID uuid := utils.FormatUUID(c.Param("uuid")) loggerInstance.Info("[INFO] 接收到获取配置文件请求", zap.Any("UUID:", uuid)) // 获取配置文件 profile, err := service.GetProfileByUUID(db, uuid) if err != nil { loggerInstance.Error("[ERROR] 获取配置文件失败:", zap.Error(err), zap.String("UUID:", uuid)) standardResponse(c, http.StatusInternalServerError, nil, err.Error()) return } // 返回配置文件信息 loggerInstance.Info("[INFO] 成功获取配置文件", zap.String("UUID:", uuid), zap.String("名称:", profile.Name)) c.JSON(http.StatusOK, service.SerializeProfile(db, loggerInstance, redisClient, *profile)) } func JoinServer(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() redisClient := redis.MustGetClient() var request JoinServerRequest clientIP := c.ClientIP() // 解析请求参数 if err := c.ShouldBindJSON(&request); err != nil { loggerInstance.Error( "解析加入服务器请求失败", zap.Error(err), zap.String("IP", clientIP), ) standardResponse(c, http.StatusBadRequest, nil, ErrInvalidRequest) return } loggerInstance.Info( "收到加入服务器请求", zap.String("服务器ID", request.ServerID), zap.String("用户UUID", request.SelectedProfile), zap.String("IP", clientIP), ) // 处理加入服务器请求 if err := service.JoinServer(db, loggerInstance, redisClient, request.ServerID, request.AccessToken, request.SelectedProfile, clientIP); err != nil { loggerInstance.Error( "加入服务器失败", zap.Error(err), zap.String("服务器ID", request.ServerID), zap.String("用户UUID", request.SelectedProfile), zap.String("IP", clientIP), ) standardResponse(c, http.StatusInternalServerError, nil, ErrJoinServerFailed) return } // 加入成功,返回204状态码 loggerInstance.Info( "加入服务器成功", zap.String("服务器ID", request.ServerID), zap.String("用户UUID", request.SelectedProfile), zap.String("IP", clientIP), ) c.Status(http.StatusNoContent) } func HasJoinedServer(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() redisClient := redis.MustGetClient() clientIP, _ := c.GetQuery("ip") // 获取并验证服务器ID参数 serverID, exists := c.GetQuery("serverId") if !exists || serverID == "" { loggerInstance.Warn("[WARN] 缺少服务器ID参数", zap.Any("IP:", clientIP)) standardResponse(c, http.StatusNoContent, nil, ErrServerIDRequired) return } // 获取并验证用户名参数 username, exists := c.GetQuery("username") if !exists || username == "" { loggerInstance.Warn("[WARN] 缺少用户名参数", zap.Any("服务器ID:", serverID), zap.Any("IP:", clientIP)) standardResponse(c, http.StatusNoContent, nil, ErrUsernameRequired) return } loggerInstance.Info("[INFO] 收到会话验证请求", zap.Any("服务器ID:", serverID), zap.Any("用户名: ", username), zap.Any("IP: ", clientIP)) // 验证玩家是否已加入服务器 if err := service.HasJoinedServer(loggerInstance, redisClient, serverID, username, clientIP); err != nil { loggerInstance.Warn("[WARN] 会话验证失败", zap.Error(err), zap.String("serverID", serverID), zap.String("username", username), zap.String("clientIP", clientIP), ) standardResponse(c, http.StatusNoContent, nil, ErrSessionVerifyFailed) return } profile, err := service.GetProfileByUUID(db, username) if err != nil { loggerInstance.Error("[ERROR] 获取用户配置文件失败: %v - 用户名: %s", zap.Error(err), // 错误详情(zap 原生支持,保留错误链) zap.String("username", username), // 结构化存储用户名(便于检索) ) standardResponse(c, http.StatusNoContent, nil, ErrProfileNotFound) return } // 返回玩家配置文件 loggerInstance.Info("[INFO] 会话验证成功 - 服务器ID: %s, 用户名: %s, UUID: %s", zap.String("serverID", serverID), // 结构化存储服务器ID zap.String("username", username), // 结构化存储用户名 zap.String("UUID", profile.UUID), // 结构化存储UUID ) c.JSON(200, service.SerializeProfile(db, loggerInstance, redisClient, *profile)) } func GetProfilesByName(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() var names []string // 解析请求参数 if err := c.ShouldBindJSON(&names); err != nil { loggerInstance.Error("[ERROR] 解析名称数组请求失败: ", zap.Error(err), ) standardResponse(c, http.StatusBadRequest, nil, ErrInvalidParams) return } loggerInstance.Info("[INFO] 接收到批量获取配置文件请求", zap.Int("名称数量:", len(names)), // 结构化存储名称数量 ) // 批量获取配置文件 profiles, err := service.GetProfilesDataByNames(db, names) if err != nil { loggerInstance.Error("[ERROR] 获取配置文件失败: ", zap.Error(err), ) } // 改造:zap 兼容原有 INFO 日志格式 loggerInstance.Info("[INFO] 成功获取配置文件", zap.Int("请求名称数:", len(names)), zap.Int("返回结果数: ", len(profiles)), ) c.JSON(http.StatusOK, profiles) } func GetMetaData(c *gin.Context) { loggerInstance := logger.MustGetLogger() redisClient := redis.MustGetClient() 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(loggerInstance, redisClient) if err != nil { loggerInstance.Error("[ERROR] 获取公钥失败: ", zap.Error(err)) standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) return } loggerInstance.Info("[INFO] 提供元数据") c.JSON(http.StatusOK, gin.H{"meta": meta, "skinDomains": skinDomains, "signaturePublickey": signature}) } func GetPlayerCertificates(c *gin.Context) { loggerInstance := logger.MustGetLogger() db := database.MustGetDB() redisClient := redis.MustGetClient() var uuid string authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header not provided"}) c.Abort() return } // 检查是否以 Bearer 开头并提取 sessionID 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 } var err error uuid, err = service.GetUUIDByAccessToken(db, tokenID) if uuid == "" { loggerInstance.Error("[ERROR] 获取玩家UUID失败: ", zap.Error(err)) standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) return } // 格式化UUID uuid = utils.FormatUUID(uuid) // 生成玩家证书 certificate, err := service.GeneratePlayerCertificate(db, loggerInstance, redisClient, uuid) if err != nil { loggerInstance.Error("[ERROR] 生成玩家证书失败: ", zap.Error(err)) standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer) return } loggerInstance.Info("[INFO] 成功生成玩家证书") c.JSON(http.StatusOK, certificate) }