Files
backend/internal/handler/yggdrasil_handler.go
2025-11-30 19:00:59 +08:00

665 lines
21 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/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
}
password, err := service.GetPasswordByUserId(db, user.ID)
if err != nil {
loggerInstance.Error("[ERROR] 邮箱查找失败", zap.Any("UserId:", user.ID), zap.Error(err))
}
// 验证密码
if password != request.Password {
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)
}