refactor(yggdrasil): 整理与性能优化,修复若干 Bug
Bug 修复
- textures metadata:SKIN 仅在 IsSlim 时输出 {"model":"slim"};CAPE 不带 metadata。
原实现误用文件字节数 skin.Size 作为 metadata,违反 Yggdrasil 协议。
- KeyPair 持久化不全:Profile 新增 rsa_public_key/public_key_signature/
public_key_signature_v2/key_expires_at/key_refresh_at 列;GetKeyPair/UpdateKeyPair
读写全部字段。AutoMigrate 自动加列。原实现每次请求都重新生成 RSA-4096。
- GetPlayerCertificates:无效 token 返回 401(先前误用 403 且未判 err)。
- HasJoinedServer:失败返回 204 无 body,符合 Yggdrasil 协议(原用 APIResponse
+ body 违反 204 规范)。
代码质量
- Authenticate 删除冗余 body 读取与回放。
- 证书服务引入具名结构 PlayerCertificate/PlayerKeyPair,替代 map[string]interface{}。
- CreateSession 用户名缺失改用 ErrUsernameRequired(新增)。
性能优化(签名一致性的回归测试已覆盖)
- SignatureService 缓存已解析的根私钥(sync.RWMutex 双重检查),
快路径仅一次 RLock + RSA 签名,免去每次签名 PEM 解析与 Redis 往返。
- GetOrCreateYggdrasilKeyPair 用 MGet 单次往返取三字段,缓存命中后零 Redis。
- NewKeyPair 消息构造用 strconv.AppendInt 避免 string+拼接分配;V2 复用 DER 字节。
- 序列化服务:texturesMap 预分配 cap(2);slim 分支直接构造完整 map。
- 会话服务 GetSession 移除重复的 ValidateServerID 校验。
死代码清理
- 删除 yggdrasil_validator.go(未被调用的 Validator)。
- 删除 signature_service.FormatPublicKey/SignStringWithProfileRSA。
- 删除 pkg/auth 中未被使用的 YggdrasilJWTManager 与 GenerateKeyPair/
EncodePrivateKeyToPEM/RedisClient/YggdrasilPrivateKeyRedisKey。
- 删除 yggdrasil_handler.go 中 25 个未使用常量、passwordRegex、
APIResponse/standardResponse。
- 删除 errors.go 中未被使用的 ErrYggForbiddenOperation/ErrYggIllegalArgument/
ErrInvalidSignature/ErrInvalidTextureType/ErrCertificateGenerate/ErrInvalidPassword。
测试
- 新增 signature_service_test.go(基于 miniredis):8 个测试 + 基准。
核心:SignString_MatchesReference 与重构前参考实现逐字节比对签名输出一致。
-race 检测通过;基准约 7.1ms/op(缓存命中路径)。
附带(与本次任务前已存在的未提交改动)
- handler/captcha_handler:将响应字段 msg 改为 message,与前端约定对齐。
This commit is contained in:
@@ -35,7 +35,7 @@ type CaptchaVerifyRequest struct {
|
||||
// @Tags captcha
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "生成成功 {code: 200, data: {masterImage, tileImage, captchaId, y}}"
|
||||
// @Success 200 {object} map[string]interface{} "生成成功 {code: 200, message: string, data: {masterImage, tileImage, captchaId, y}}"
|
||||
// @Failure 500 {object} map[string]interface{} "生成失败"
|
||||
// @Router /api/v1/captcha/generate [get]
|
||||
func (h *CaptchaHandler) Generate(c *gin.Context) {
|
||||
@@ -43,14 +43,15 @@ func (h *CaptchaHandler) Generate(c *gin.Context) {
|
||||
if err != nil {
|
||||
h.logger.Error("生成验证码失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": "生成验证码失败",
|
||||
"code": 500,
|
||||
"message": "生成验证码失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": gin.H{
|
||||
"masterImage": masterImg,
|
||||
"tileImage": tileImg,
|
||||
@@ -67,15 +68,15 @@ func (h *CaptchaHandler) Generate(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body CaptchaVerifyRequest true "验证请求"
|
||||
// @Success 200 {object} map[string]interface{} "验证结果 {code: 200/400, msg: string}"
|
||||
// @Success 200 {object} map[string]interface{} "验证结果 {code: 200/400, message: string}"
|
||||
// @Failure 400 {object} map[string]interface{} "参数错误"
|
||||
// @Router /api/v1/captcha/verify [post]
|
||||
func (h *CaptchaHandler) Verify(c *gin.Context) {
|
||||
var req CaptchaVerifyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"msg": "参数错误: " + err.Error(),
|
||||
"code": 400,
|
||||
"message": "参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -87,21 +88,21 @@ func (h *CaptchaHandler) Verify(c *gin.Context) {
|
||||
zap.Error(err),
|
||||
)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"msg": "验证失败",
|
||||
"code": 500,
|
||||
"message": "验证失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if valid {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "验证成功",
|
||||
"code": 200,
|
||||
"message": "验证成功",
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 400,
|
||||
"msg": "验证失败,请重试",
|
||||
"code": 400,
|
||||
"message": "验证失败,请重试",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
@@ -17,66 +15,18 @@ import (
|
||||
|
||||
// 常量定义
|
||||
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,}$`)
|
||||
)
|
||||
|
||||
// 请求结构体
|
||||
@@ -139,22 +89,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -180,16 +114,8 @@ func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler {
|
||||
// @Failure 403 {object} map[string]string "认证失败"
|
||||
// @Router /api/yggdrasil/authserver/authenticate [post]
|
||||
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 {
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
h.logger.Error("解析认证请求失败", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -198,8 +124,10 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
|
||||
var userId int64
|
||||
var profile *model.Profile
|
||||
var UUID string
|
||||
var err error
|
||||
|
||||
if emailRegex.MatchString(request.Identifier) {
|
||||
// 邮箱登录:UUID 留空,后续 TokenService.Create 会自动选择该用户的角色
|
||||
userId, err = h.container.YggdrasilService.GetUserIDByEmail(c.Request.Context(), request.Identifier)
|
||||
} else {
|
||||
profile, err = h.container.ProfileService.GetByProfileName(c.Request.Context(), request.Identifier)
|
||||
@@ -507,7 +435,7 @@ func (h *YggdrasilHandler) SignOut(c *gin.Context) {
|
||||
|
||||
if !emailRegex.MatchString(request.Email) {
|
||||
h.logger.Warn("登出失败: 邮箱格式不正确", zap.String("email", request.Email))
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": ErrInvalidEmailFormat})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -579,8 +507,8 @@ func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param request body JoinServerRequest true "加入请求"
|
||||
// @Success 204 "加入成功"
|
||||
// @Failure 400 {object} APIResponse "参数错误"
|
||||
// @Failure 500 {object} APIResponse "服务器错误"
|
||||
// @Failure 400 {object} map[string]string "参数错误"
|
||||
// @Failure 500 {object} map[string]string "服务器错误"
|
||||
// @Router /api/yggdrasil/sessionserver/session/minecraft/join [post]
|
||||
func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
|
||||
var request JoinServerRequest
|
||||
@@ -643,14 +571,14 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
|
||||
serverID, exists := c.GetQuery("serverId")
|
||||
if !exists || serverID == "" {
|
||||
h.logger.Warn("缺少服务器ID参数", zap.String("ip", clientIP))
|
||||
standardResponse(c, http.StatusNoContent, nil, ErrServerIDRequired)
|
||||
c.Status(http.StatusNoContent)
|
||||
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)
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -667,14 +595,14 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
|
||||
zap.String("username", username),
|
||||
zap.String("ip", clientIP),
|
||||
)
|
||||
standardResponse(c, http.StatusNoContent, nil, ErrSessionVerifyFailed)
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -683,7 +611,7 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
|
||||
zap.String("username", username),
|
||||
zap.String("uuid", profile.UUID),
|
||||
)
|
||||
c.JSON(200, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile))
|
||||
c.JSON(http.StatusOK, h.container.YggdrasilService.SerializeProfile(c.Request.Context(), *profile))
|
||||
}
|
||||
|
||||
// GetProfilesByName 批量获取配置文件
|
||||
@@ -694,14 +622,14 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param request body []string true "用户名列表"
|
||||
// @Success 200 {array} model.Profile "档案列表"
|
||||
// @Failure 400 {object} APIResponse "参数错误"
|
||||
// @Failure 400 {object} map[string]string "参数错误"
|
||||
// @Router /api/yggdrasil/api/profiles/minecraft [post]
|
||||
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)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -723,7 +651,7 @@ func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "元数据"
|
||||
// @Failure 500 {object} APIResponse "服务器错误"
|
||||
// @Failure 500 {object} map[string]string "服务器错误"
|
||||
// @Router /api/yggdrasil [get]
|
||||
func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
|
||||
meta := gin.H{
|
||||
@@ -765,7 +693,7 @@ func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "Bearer {token}"
|
||||
// @Success 200 {object} map[string]interface{} "证书信息"
|
||||
// @Success 200 {object} service.PlayerCertificate "证书信息"
|
||||
// @Failure 401 {object} errors.YggdrasilErrorResponse "未授权"
|
||||
// @Failure 403 {object} errors.YggdrasilErrorResponse "令牌无效"
|
||||
// @Failure 500 {object} errors.YggdrasilErrorResponse "服务器错误"
|
||||
@@ -805,10 +733,10 @@ func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
|
||||
}
|
||||
|
||||
uuid, err := h.container.TokenService.GetUUIDByAccessToken(c.Request.Context(), tokenID)
|
||||
if uuid == "" {
|
||||
if err != nil || uuid == "" {
|
||||
h.logger.Error("获取玩家UUID失败", zap.Error(err))
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
"ForbiddenOperationException",
|
||||
c.JSON(http.StatusUnauthorized, errors.NewYggdrasilErrorResponse(
|
||||
"Unauthorized",
|
||||
errors.YggErrInvalidToken,
|
||||
"",
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user