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,与前端约定对齐。
109 lines
2.8 KiB
Go
109 lines
2.8 KiB
Go
package auth
|
||
|
||
import (
|
||
"crypto/rsa"
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
)
|
||
|
||
// YggdrasilJWTService Yggdrasil JWT服务(使用RSA512)
|
||
type YggdrasilJWTService struct {
|
||
privateKey *rsa.PrivateKey
|
||
publicKey *rsa.PublicKey
|
||
issuer string
|
||
}
|
||
|
||
// NewYggdrasilJWTService 创建新的Yggdrasil JWT服务
|
||
func NewYggdrasilJWTService(privateKey *rsa.PrivateKey, issuer string) *YggdrasilJWTService {
|
||
if issuer == "" {
|
||
issuer = "carrotskin"
|
||
}
|
||
return &YggdrasilJWTService{
|
||
privateKey: privateKey,
|
||
publicKey: &privateKey.PublicKey,
|
||
issuer: issuer,
|
||
}
|
||
}
|
||
|
||
// YggdrasilTokenClaims Yggdrasil Token声明
|
||
type YggdrasilTokenClaims struct {
|
||
Version int `json:"version"` // 版本号,用于失效旧Token
|
||
UserID int64 `json:"user_id"` // 用户ID
|
||
ProfileID string `json:"profile_id,omitempty"` // 选中的Profile UUID
|
||
jwt.RegisteredClaims
|
||
}
|
||
|
||
// StaleTokenPolicy Token过期策略
|
||
type StaleTokenPolicy int
|
||
|
||
const (
|
||
StalePolicyAllow StaleTokenPolicy = iota // 允许过期的Token(但未过StaleAt)
|
||
StalePolicyDeny // 拒绝过期的Token
|
||
)
|
||
|
||
// GenerateAccessToken 生成AccessToken JWT
|
||
func (j *YggdrasilJWTService) GenerateAccessToken(
|
||
userID int64,
|
||
clientUUID string,
|
||
version int,
|
||
profileID string,
|
||
expiresAt time.Time,
|
||
staleAt time.Time,
|
||
) (string, error) {
|
||
claims := YggdrasilTokenClaims{
|
||
Version: version,
|
||
UserID: userID,
|
||
ProfileID: profileID,
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
Subject: clientUUID,
|
||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||
Issuer: j.issuer,
|
||
},
|
||
}
|
||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
||
return token.SignedString(j.privateKey)
|
||
}
|
||
|
||
// ParseAccessToken 解析AccessToken JWT
|
||
func (j *YggdrasilJWTService) ParseAccessToken(accessToken string, stalePolicy StaleTokenPolicy) (*YggdrasilTokenClaims, error) {
|
||
token, err := jwt.ParseWithClaims(accessToken, &YggdrasilTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||
// 验证签名算法
|
||
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||
return nil, errors.New("不支持的签名算法,需要使用RSA")
|
||
}
|
||
return j.publicKey, nil
|
||
})
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if !token.Valid {
|
||
return nil, errors.New("无效的token")
|
||
}
|
||
|
||
claims, ok := token.Claims.(*YggdrasilTokenClaims)
|
||
if !ok {
|
||
return nil, errors.New("无法解析token声明")
|
||
}
|
||
|
||
// 检查StaleAt(如果设置了拒绝过期策略)
|
||
if stalePolicy == StalePolicyDeny && claims.ExpiresAt != nil {
|
||
if time.Now().After(claims.ExpiresAt.Time) {
|
||
return nil, errors.New("token已过期")
|
||
}
|
||
}
|
||
|
||
return claims, nil
|
||
}
|
||
|
||
// GetPublicKey 获取公钥
|
||
func (j *YggdrasilJWTService) GetPublicKey() *rsa.PublicKey {
|
||
return j.publicKey
|
||
}
|