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,与前端约定对齐。
149 lines
4.8 KiB
Go
149 lines
4.8 KiB
Go
// Package errors 定义应用程序的错误类型
|
|
package errors
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// 预定义错误
|
|
var (
|
|
// 用户相关错误
|
|
ErrUserNotFound = errors.New("用户不存在")
|
|
ErrUserAlreadyExists = errors.New("用户已存在")
|
|
ErrEmailAlreadyExists = errors.New("邮箱已被注册")
|
|
ErrAccountDisabled = errors.New("账号已被禁用")
|
|
|
|
// 认证相关错误
|
|
ErrUnauthorized = errors.New("未授权")
|
|
ErrInvalidToken = errors.New("无效的令牌")
|
|
ErrTokenExpired = errors.New("令牌已过期")
|
|
|
|
// 档案相关错误
|
|
ErrProfileNotFound = errors.New("档案不存在")
|
|
ErrProfileNameExists = errors.New("角色名已被使用")
|
|
ErrProfileLimitReached = errors.New("已达档案数量上限")
|
|
ErrProfileNoPermission = errors.New("无权操作此档案")
|
|
|
|
// 材质相关错误
|
|
ErrTextureNotFound = errors.New("材质不存在")
|
|
ErrTextureExists = errors.New("该材质已存在")
|
|
ErrTextureLimitReached = errors.New("已达材质数量上限")
|
|
ErrTextureNoPermission = errors.New("无权操作此材质")
|
|
|
|
// 验证码相关错误
|
|
ErrInvalidVerificationCode = errors.New("验证码错误或已过期")
|
|
ErrTooManyAttempts = errors.New("尝试次数过多")
|
|
ErrSendTooFrequent = errors.New("发送过于频繁")
|
|
|
|
// URL验证相关错误
|
|
ErrInvalidURL = errors.New("无效的URL格式")
|
|
ErrDomainNotAllowed = errors.New("URL域名不在允许的列表中")
|
|
|
|
// 存储相关错误
|
|
ErrStorageUnavailable = errors.New("存储服务不可用")
|
|
ErrUploadFailed = errors.New("上传失败")
|
|
|
|
// Yggdrasil相关错误
|
|
ErrPasswordMismatch = errors.New("密码错误")
|
|
ErrPasswordNotSet = errors.New("未生成密码")
|
|
ErrInvalidServerID = errors.New("服务器ID格式无效")
|
|
ErrSessionNotFound = errors.New("会话不存在或已过期")
|
|
ErrSessionMismatch = errors.New("会话验证失败")
|
|
ErrUsernameMismatch = errors.New("用户名不匹配")
|
|
ErrUsernameRequired = errors.New("用户名不能为空")
|
|
ErrIPMismatch = errors.New("IP地址不匹配")
|
|
ErrInvalidAccessToken = errors.New("访问令牌无效")
|
|
ErrProfileMismatch = errors.New("selectedProfile与Token不匹配")
|
|
ErrUUIDRequired = errors.New("UUID不能为空")
|
|
|
|
// 通用错误
|
|
ErrBadRequest = errors.New("请求参数错误")
|
|
ErrInternalServer = errors.New("服务器内部错误")
|
|
ErrNotFound = errors.New("资源不存在")
|
|
ErrForbidden = errors.New("权限不足")
|
|
)
|
|
|
|
// AppError 应用错误类型,包含错误码和消息
|
|
type AppError struct {
|
|
Code int // HTTP状态码
|
|
Message string // 用户可见的错误消息
|
|
Err error // 原始错误(用于日志)
|
|
}
|
|
|
|
// Error 实现error接口
|
|
func (e *AppError) Error() string {
|
|
if e.Err != nil {
|
|
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
|
}
|
|
return e.Message
|
|
}
|
|
|
|
// Unwrap 支持errors.Is和errors.As
|
|
func (e *AppError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// NewAppError 创建新的应用错误
|
|
func NewAppError(code int, message string, err error) *AppError {
|
|
return &AppError{
|
|
Code: code,
|
|
Message: message,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
// NewBadRequest 创建400错误
|
|
func NewBadRequest(message string, err error) *AppError {
|
|
return NewAppError(400, message, err)
|
|
}
|
|
|
|
// NewUnauthorized 创建401错误
|
|
func NewUnauthorized(message string) *AppError {
|
|
return NewAppError(401, message, nil)
|
|
}
|
|
|
|
// NewForbidden 创建403错误
|
|
func NewForbidden(message string) *AppError {
|
|
return NewAppError(403, message, nil)
|
|
}
|
|
|
|
// NewNotFound 创建404错误
|
|
func NewNotFound(message string) *AppError {
|
|
return NewAppError(404, message, nil)
|
|
}
|
|
|
|
// NewInternalError 创建500错误
|
|
func NewInternalError(message string, err error) *AppError {
|
|
return NewAppError(500, message, err)
|
|
}
|
|
|
|
// 注意:原此处的 Is/As/Wrap 函数(透传标准库)已删除。
|
|
// 请直接使用标准库 errors.Is / errors.As / fmt.Errorf。
|
|
|
|
// YggdrasilErrorResponse Yggdrasil协议标准错误响应格式
|
|
type YggdrasilErrorResponse struct {
|
|
Error string `json:"error"` // 错误的简要描述(机器可读)
|
|
ErrorMessage string `json:"errorMessage"` // 错误的详细信息(人类可读)
|
|
Cause string `json:"cause,omitempty"` // 该错误的原因(可选)
|
|
}
|
|
|
|
// NewYggdrasilErrorResponse 创建Yggdrasil标准错误响应
|
|
func NewYggdrasilErrorResponse(error, errorMessage, cause string) *YggdrasilErrorResponse {
|
|
return &YggdrasilErrorResponse{
|
|
Error: error,
|
|
ErrorMessage: errorMessage,
|
|
Cause: cause,
|
|
}
|
|
}
|
|
|
|
// YggdrasilErrorCodes Yggdrasil协议错误码常量
|
|
const (
|
|
// ForbiddenOperationException 错误消息
|
|
YggErrInvalidToken = "Invalid token."
|
|
YggErrInvalidCredentials = "Invalid credentials. Invalid username or password."
|
|
|
|
// IllegalArgumentException 错误消息
|
|
YggErrProfileAlreadyAssigned = "Access token already has a profile assigned."
|
|
)
|