feat(yggdrasil): implement standard error responses and UUID format improvements
- Add YggdrasilErrorResponse struct and standard error codes for protocol compliance - Change UUID storage from varchar(36) to varchar(32) for unsigned format - Add utility functions: GenerateUUID, FormatUUIDToNoDash, RandomHex - Support unsigned query parameter in GetProfileByUUID endpoint - Improve refresh token response with available profiles list - Fix key pair retrieval to use correct database column (rsa_private_key) - Update UUID validator to accept both 32-char and 36-char formats - Add SignStringWithProfileRSA method for profile-specific signing - Fix profile assignment validation in refresh token flow
This commit is contained in:
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"bytes"
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/pkg/utils"
|
||||
"io"
|
||||
@@ -129,10 +130,11 @@ type (
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -202,7 +204,11 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
|
||||
profile, err = h.container.ProfileRepo.FindByName(c.Request.Context(), 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()})
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidCredentials,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
userId = profile.UserID
|
||||
@@ -211,13 +217,21 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
|
||||
|
||||
if err != nil {
|
||||
h.logger.Warn("认证失败: 用户不存在", zap.String("identifier", request.Identifier), zap.Error(err))
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "用户不存在"})
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidCredentials,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.container.YggdrasilService.VerifyPassword(c.Request.Context(), request.Password, userId); err != nil {
|
||||
h.logger.Warn("认证失败: 密码错误", zap.Error(err))
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": ErrWrongPassword})
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidCredentials,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,7 +278,7 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param request body ValidTokenRequest true "验证请求"
|
||||
// @Success 204 "令牌有效"
|
||||
// @Failure 403 {object} map[string]bool "令牌无效"
|
||||
// @Failure 403 {object} errors.YggdrasilErrorResponse "令牌无效"
|
||||
// @Router /api/v1/yggdrasil/authserver/validate [post]
|
||||
func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
|
||||
var request ValidTokenRequest
|
||||
@@ -276,10 +290,14 @@ func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
|
||||
|
||||
if h.container.TokenService.Validate(c.Request.Context(), request.AccessToken, request.ClientToken) {
|
||||
h.logger.Info("令牌验证成功", zap.String("accessToken", request.AccessToken))
|
||||
c.JSON(http.StatusNoContent, gin.H{"valid": true})
|
||||
c.JSON(http.StatusNoContent, gin.H{})
|
||||
} else {
|
||||
h.logger.Warn("令牌验证失败", zap.String("accessToken", request.AccessToken))
|
||||
c.JSON(http.StatusForbidden, gin.H{"valid": false})
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,19 +319,33 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
UUID, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), request.AccessToken)
|
||||
// 获取用户ID和当前绑定的Profile UUID
|
||||
userID, err := h.container.TokenService.GetUserIDByAccessToken(c.Request.Context(), 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()})
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
userID, _ := h.container.TokenService.GetUserIDByAccessToken(c.Request.Context(), request.AccessToken)
|
||||
UUID = utils.FormatUUID(UUID)
|
||||
|
||||
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), UUID)
|
||||
currentUUID, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), request.AccessToken)
|
||||
if err != nil {
|
||||
h.logger.Error("刷新令牌失败: 无法获取用户信息", zap.Error(err))
|
||||
h.logger.Warn("刷新令牌失败: 无法获取当前Profile", zap.String("token", request.AccessToken), zap.Error(err))
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户的所有可用profiles
|
||||
availableProfiles, err := h.container.ProfileService.GetByUserID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.logger.Error("刷新令牌失败: 无法获取用户profiles", zap.Error(err), zap.Int64("userId", userID))
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -322,6 +354,7 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
|
||||
var userData map[string]interface{}
|
||||
var profileID string
|
||||
|
||||
// 处理selectedProfile
|
||||
if request.SelectedProfile != nil {
|
||||
profileIDValue, ok := request.SelectedProfile["id"]
|
||||
if !ok {
|
||||
@@ -337,25 +370,69 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
profileID = utils.FormatUUID(profileID)
|
||||
// 确保profileID是32位无符号格式(用于向后兼容)
|
||||
profileID = utils.FormatUUIDToNoDash(profileID)
|
||||
|
||||
// 根据Yggdrasil规范:当指定selectedProfile时,原令牌所绑定的角色必须为空
|
||||
// 如果原令牌已绑定角色,则不允许更换角色,但允许选择相同的角色以兼容部分启动器
|
||||
if currentUUID != "" && currentUUID != profileID {
|
||||
h.logger.Warn("刷新令牌失败: 令牌已绑定其他角色,不允许更换",
|
||||
zap.Int64("userId", userID),
|
||||
zap.String("currentUUID", currentUUID),
|
||||
zap.String("requestedUUID", profileID),
|
||||
)
|
||||
c.JSON(http.StatusBadRequest, errors.NewYggdrasilErrorResponse(
|
||||
"IllegalArgumentException",
|
||||
errors.YggErrProfileAlreadyAssigned,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// 验证profile是否属于该用户
|
||||
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), profileID)
|
||||
if err != nil {
|
||||
h.logger.Warn("刷新令牌失败: Profile不存在", zap.String("profileId", profileID), zap.Error(err))
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
if profile.UserID != userID {
|
||||
h.logger.Warn("刷新令牌失败: 用户不匹配",
|
||||
zap.Int64("userId", userID),
|
||||
zap.Int64("profileUserId", profile.UserID),
|
||||
)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": ErrUserNotMatch})
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
profileData = h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile)
|
||||
} else {
|
||||
// 如果未指定selectedProfile,使用当前token绑定的profile(如果存在)
|
||||
if currentUUID != "" {
|
||||
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), currentUUID)
|
||||
if err == nil && profile != nil {
|
||||
profileID = currentUUID
|
||||
profileData = h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息(如果请求)
|
||||
user, _ := h.container.UserService.GetByID(c.Request.Context(), userID)
|
||||
if request.RequestUser && user != nil {
|
||||
userData = h.container.YggdrasilService.SerializeUser(c.Request.Context(), user, UUID)
|
||||
userData = h.container.YggdrasilService.SerializeUser(c.Request.Context(), user, currentUUID)
|
||||
}
|
||||
|
||||
// 刷新token
|
||||
newAccessToken, newClientToken, err := h.container.TokenService.Refresh(c.Request.Context(),
|
||||
request.AccessToken,
|
||||
request.ClientToken,
|
||||
@@ -363,16 +440,27 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("刷新令牌失败", zap.Error(err), zap.Int64("userId", userID))
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
// 序列化可用profiles
|
||||
availableProfilesData := make([]map[string]interface{}, 0, len(availableProfiles))
|
||||
for _, p := range availableProfiles {
|
||||
availableProfilesData = append(availableProfilesData, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *p))
|
||||
}
|
||||
|
||||
h.logger.Info("刷新令牌成功", zap.Int64("userId", userID))
|
||||
c.JSON(http.StatusOK, RefreshResponse{
|
||||
AccessToken: newAccessToken,
|
||||
ClientToken: newClientToken,
|
||||
SelectedProfile: profileData,
|
||||
User: userData,
|
||||
AccessToken: newAccessToken,
|
||||
ClientToken: newClientToken,
|
||||
SelectedProfile: profileData,
|
||||
AvailableProfiles: availableProfilesData,
|
||||
User: userData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -431,7 +519,11 @@ func (h *YggdrasilHandler) SignOut(c *gin.Context) {
|
||||
|
||||
if err := h.container.YggdrasilService.VerifyPassword(c.Request.Context(), request.Password, user.ID); err != nil {
|
||||
h.logger.Warn("登出失败: 密码错误", zap.Int64("userId", user.ID))
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": ErrWrongPassword})
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidCredentials,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -447,22 +539,35 @@ func (h *YggdrasilHandler) SignOut(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param uuid path string true "用户UUID"
|
||||
// @Param unsigned query string false "是否不返回签名(true/false,默认false)"
|
||||
// @Success 200 {object} map[string]interface{} "档案信息"
|
||||
// @Failure 500 {object} APIResponse "服务器错误"
|
||||
// @Failure 404 {object} errors.YggdrasilErrorResponse "档案不存在"
|
||||
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/profile/{uuid} [get]
|
||||
func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
|
||||
uuid := utils.FormatUUID(c.Param("uuid"))
|
||||
uuid := c.Param("uuid")
|
||||
h.logger.Info("获取配置文件请求", zap.String("uuid", uuid))
|
||||
|
||||
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), uuid)
|
||||
if err != nil {
|
||||
h.logger.Error("获取配置文件失败", zap.Error(err), zap.String("uuid", uuid))
|
||||
standardResponse(c, http.StatusInternalServerError, nil, err.Error())
|
||||
c.JSON(http.StatusNotFound, errors.NewYggdrasilErrorResponse(
|
||||
"Not Found",
|
||||
"Profile not found",
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("成功获取配置文件", zap.String("uuid", uuid), zap.String("name", profile.Name))
|
||||
c.JSON(http.StatusOK, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile))
|
||||
// 读取 unsigned 查询参数
|
||||
unsignedParam := c.DefaultQuery("unsigned", "false")
|
||||
unsigned := unsignedParam == "true"
|
||||
|
||||
h.logger.Info("成功获取配置文件",
|
||||
zap.String("uuid", uuid),
|
||||
zap.String("name", profile.Name),
|
||||
zap.Bool("unsigned", unsigned))
|
||||
|
||||
c.JSON(http.StatusOK, h.container.YggdrasilService.SerializeProfileWithUnsigned(c.Request.Context(), *profile, unsigned))
|
||||
}
|
||||
|
||||
// JoinServer 加入服务器
|
||||
@@ -482,7 +587,11 @@ func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
h.logger.Error("解析加入服务器请求失败", zap.Error(err), zap.String("ip", clientIP))
|
||||
standardResponse(c, http.StatusBadRequest, nil, ErrInvalidRequest)
|
||||
c.JSON(http.StatusBadRequest, errors.NewYggdrasilErrorResponse(
|
||||
"Bad Request",
|
||||
"Invalid request format",
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -499,7 +608,11 @@ func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
|
||||
zap.String("userUUID", request.SelectedProfile),
|
||||
zap.String("ip", clientIP),
|
||||
)
|
||||
standardResponse(c, http.StatusInternalServerError, nil, ErrJoinServerFailed)
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -557,7 +670,7 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := h.container.ProfileService.GetByUUID(c.Request.Context(), username)
|
||||
profile, err := h.container.ProfileService.GetByProfileName(c.Request.Context(), username)
|
||||
if err != nil {
|
||||
h.logger.Error("获取用户配置文件失败", zap.Error(err), zap.String("username", username))
|
||||
standardResponse(c, http.StatusNoContent, nil, ErrProfileNotFound)
|
||||
@@ -628,7 +741,11 @@ func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
|
||||
signature, err := h.container.YggdrasilService.GetPublicKey(c.Request.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("获取公钥失败", zap.Error(err))
|
||||
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
|
||||
c.JSON(http.StatusInternalServerError, errors.NewYggdrasilErrorResponse(
|
||||
"Internal Server Error",
|
||||
"Failed to get public key",
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -648,27 +765,40 @@ func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "Bearer {token}"
|
||||
// @Success 200 {object} map[string]interface{} "证书信息"
|
||||
// @Failure 401 {object} map[string]string "未授权"
|
||||
// @Failure 500 {object} APIResponse "服务器错误"
|
||||
// @Failure 401 {object} errors.YggdrasilErrorResponse "未授权"
|
||||
// @Failure 403 {object} errors.YggdrasilErrorResponse "令牌无效"
|
||||
// @Failure 500 {object} errors.YggdrasilErrorResponse "服务器错误"
|
||||
// @Router /api/v1/yggdrasil/minecraftservices/player/certificates [post]
|
||||
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.JSON(http.StatusUnauthorized, errors.NewYggdrasilErrorResponse(
|
||||
"Unauthorized",
|
||||
"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.JSON(http.StatusUnauthorized, errors.NewYggdrasilErrorResponse(
|
||||
"Unauthorized",
|
||||
"Invalid Authorization format",
|
||||
"",
|
||||
))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenID := authHeader[len(bearerPrefix):]
|
||||
if tokenID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization format"})
|
||||
c.JSON(http.StatusUnauthorized, errors.NewYggdrasilErrorResponse(
|
||||
"Unauthorized",
|
||||
"Invalid Authorization format",
|
||||
"",
|
||||
))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
@@ -676,16 +806,24 @@ func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
|
||||
uuid, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), tokenID)
|
||||
if uuid == "" {
|
||||
h.logger.Error("获取玩家UUID失败", zap.Error(err))
|
||||
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
uuid = utils.FormatUUID(uuid)
|
||||
// UUID已经是32位无符号格式,无需转换
|
||||
|
||||
certificate, err := h.container.YggdrasilService.GeneratePlayerCertificate(c.Request.Context(), uuid)
|
||||
if err != nil {
|
||||
h.logger.Error("生成玩家证书失败", zap.Error(err))
|
||||
standardResponse(c, http.StatusInternalServerError, nil, ErrInternalServer)
|
||||
c.JSON(http.StatusInternalServerError, errors.NewYggdrasilErrorResponse(
|
||||
"Internal Server Error",
|
||||
"Failed to generate player certificate",
|
||||
"",
|
||||
))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user