0c9f256dfc
对应体检报告 #10-#24,一次性完成 v1.1.0 全部 13 项: - #10 storage/cache/redis/jwt/logger 五组件 Manager 化(XxxManager + DefaultXxx + SetDefaultXxxManager,包级 facade 保留兼容) - #11 删除 wire 包 + 清理 StartServerWithPort/GracefulShutdown 双轨 - #12 Lifecycle Hooks(OnInit/OnStart/OnReady/OnStop) - #13 Server 参数配置化(timeout/TLS/unix_socket/response_mode) - #14 JWTConfig.Expire 改 time.Duration + 删 AppConfig.TokenExpire - #15 response Mode 开关(ModeBusiness/ModeREST) - #16 Config.Validate 启动期校验 - #17 livez/readyz 探针路由 - #18 Prometheus metrics 中间件 + /metrics - #19 请求级 Timeout 中间件 - #21 主库探活 + replica 健康剔除 - #22 App.Go + in-flight goroutine 管理 - #24 RequestID 默认装入 + #23 核对 Breaking:删 wire、删 TokenExpire、JWTConfig.Expire 类型变更、删双轨函数。 详见 CHANGELOG 升级说明。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
333 lines
9.0 KiB
Go
333 lines
9.0 KiB
Go
package jwt
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rand"
|
||
"encoding/base64"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/config"
|
||
"github.com/EthanCodeCraft/xlgo-core/database"
|
||
"github.com/golang-jwt/jwt/v5"
|
||
"github.com/redis/go-redis/v9"
|
||
)
|
||
|
||
// Claims JWT 声明
|
||
type Claims struct {
|
||
UserID uint `json:"user_id"`
|
||
Username string `json:"username"`
|
||
Role string `json:"role"` // admin 或 staff
|
||
UserType string `json:"user_type"` // super_admin, admin, staff
|
||
JTI string `json:"jti"` // JWT ID(唯一标识,用于黑名单)
|
||
jwt.RegisteredClaims
|
||
}
|
||
|
||
var (
|
||
//ErrTokenExpired 令牌已过期
|
||
ErrTokenExpired = errors.New("令牌已过期")
|
||
//ErrTokenInvalid 令牌无效
|
||
ErrTokenInvalid = errors.New("令牌无效")
|
||
//ErrTokenMalformed 令牌格式错误
|
||
ErrTokenMalformed = errors.New("令牌格式错误")
|
||
//ErrTokenNotValidYet 令牌尚未生效
|
||
ErrTokenNotValidYet = errors.New("令牌尚未生效")
|
||
//ErrTokenRevoked 令牌已被撤销
|
||
ErrTokenRevoked = errors.New("令牌已被撤销")
|
||
)
|
||
|
||
// generateJTI 生成唯一的 JWT ID
|
||
func generateJTI() (string, error) {
|
||
bytes := make([]byte, 16)
|
||
if _, err := rand.Read(bytes); err != nil {
|
||
return "", fmt.Errorf("生成 JTI 失败: %w", err)
|
||
}
|
||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||
}
|
||
|
||
// TokenBlacklist Token 黑名单管理(使用 JTI 优化)。
|
||
// client 为 nil 时回退到 database.GetRedis(),兼容存量未注入场景。
|
||
type TokenBlacklist struct {
|
||
client *redis.Client
|
||
}
|
||
|
||
// NewTokenBlacklist 创建黑名单实例,client 可为 nil(懒取全局 Redis)。
|
||
func NewTokenBlacklist(client *redis.Client) *TokenBlacklist {
|
||
return &TokenBlacklist{client: client}
|
||
}
|
||
|
||
func (tb *TokenBlacklist) redisClient() *redis.Client {
|
||
if tb != nil && tb.client != nil {
|
||
return tb.client
|
||
}
|
||
return database.GetRedis()
|
||
}
|
||
|
||
// Add 将 Token 的 JTI 加入黑名单
|
||
// 参数: jti JWT ID,expiry Token 过期时间
|
||
func (tb *TokenBlacklist) Add(jti string, expiry time.Time) error {
|
||
client := tb.redisClient()
|
||
if client == nil {
|
||
// Redis 未启用,跳过黑名单
|
||
return nil
|
||
}
|
||
|
||
ctx := context.Background()
|
||
ttl := time.Until(expiry)
|
||
if ttl <= 0 {
|
||
// Token 已过期,无需加入黑名单
|
||
return nil
|
||
}
|
||
|
||
// 使用 JTI 作为键名(约24字节),而非完整 Token(数百字节)
|
||
key := fmt.Sprintf("jwt_bl:%s", jti)
|
||
return client.Set(ctx, key, "1", ttl).Err()
|
||
}
|
||
|
||
// IsBlacklisted 检查 JTI 是否在黑名单中
|
||
func (tb *TokenBlacklist) IsBlacklisted(jti string) bool {
|
||
client := tb.redisClient()
|
||
if client == nil {
|
||
// Redis 未启用,不检查黑名单
|
||
return false
|
||
}
|
||
|
||
ctx := context.Background()
|
||
key := fmt.Sprintf("jwt_bl:%s", jti)
|
||
return client.Exists(ctx, key).Val() > 0
|
||
}
|
||
|
||
// Manager JWT 管理器(#10)。持有独立的 TokenBlacklist,
|
||
// 支持多实例(如区分 user-token 与 refresh-token 黑名单)。
|
||
type Manager struct {
|
||
mu sync.Mutex
|
||
blacklist *TokenBlacklist
|
||
}
|
||
|
||
// DefaultJWT 默认 JWT 管理器,包级 facade 代理到它的 blacklist。
|
||
var DefaultJWT = NewJWTManager()
|
||
|
||
// NewJWTManager 创建 JWT 管理器实例(blacklist 懒取全局 Redis)。
|
||
func NewJWTManager() *Manager {
|
||
return &Manager{blacklist: NewTokenBlacklist(nil)}
|
||
}
|
||
|
||
// NewJWTManagerWithRedis 创建 JWT 管理器并注入指定 Redis 客户端(用于多 Redis/测试隔离)。
|
||
func NewJWTManagerWithRedis(client *redis.Client) *Manager {
|
||
return &Manager{blacklist: NewTokenBlacklist(client)}
|
||
}
|
||
|
||
// SetDefaultJWTManager 提升指定 Manager 为全局默认。
|
||
func SetDefaultJWTManager(m *Manager) {
|
||
if m != nil {
|
||
DefaultJWT = m
|
||
tokenBlacklist = m.blacklist
|
||
}
|
||
}
|
||
|
||
// Blacklist 返回 Manager 持有的黑名单实例。
|
||
func (m *Manager) Blacklist() *TokenBlacklist {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
return m.blacklist
|
||
}
|
||
|
||
// 全局黑名单实例(指向 DefaultJWT 的 blacklist,兼容存量包级函数)
|
||
var tokenBlacklist = DefaultJWT.blacklist
|
||
|
||
// GenerateToken 生成 JWT Token
|
||
func GenerateToken(userID uint, username, role, userType string) (string, error) {
|
||
cfg := config.Get()
|
||
|
||
// 生成唯一的 JWT ID
|
||
jti, err := generateJTI()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
claims := Claims{
|
||
UserID: userID,
|
||
Username: username,
|
||
Role: role,
|
||
UserType: userType,
|
||
JTI: jti,
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(cfg.JWT.Expire)),
|
||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||
Issuer: issuerOrDefault(cfg.JWT.Issuer),
|
||
ID: jti, // 同时设置到 RegisteredClaims.ID
|
||
},
|
||
}
|
||
|
||
token := jwt.NewWithClaims(signingMethod(cfg.JWT.Algorithm), claims)
|
||
return token.SignedString([]byte(cfg.JWT.Secret))
|
||
}
|
||
|
||
// GenerateTokenWithCustomExpiry 生成带自定义过期时间的 Token
|
||
func GenerateTokenWithCustomExpiry(userID uint, username, role, userType string, expireSeconds int) (string, error) {
|
||
cfg := config.Get()
|
||
|
||
jti, err := generateJTI()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
claims := Claims{
|
||
UserID: userID,
|
||
Username: username,
|
||
Role: role,
|
||
UserType: userType,
|
||
JTI: jti,
|
||
RegisteredClaims: jwt.RegisteredClaims{
|
||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireSeconds) * time.Second)),
|
||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||
Issuer: issuerOrDefault(cfg.JWT.Issuer),
|
||
ID: jti,
|
||
},
|
||
}
|
||
|
||
token := jwt.NewWithClaims(signingMethod(cfg.JWT.Algorithm), claims)
|
||
return token.SignedString([]byte(cfg.JWT.Secret))
|
||
}
|
||
|
||
// issuerOrDefault 返回配置的 issuer,未配置时回退 "xlgo"。
|
||
func issuerOrDefault(issuer string) string {
|
||
if issuer == "" {
|
||
return "xlgo"
|
||
}
|
||
return issuer
|
||
}
|
||
|
||
// signingMethod 根据 algorithm 配置返回 HMAC 签名方法。
|
||
// 目前支持 HS256(默认)/HS384/HS512;其它值回退 HS256。
|
||
// RS256 等非对称算法需扩展密钥类型,暂不支持。
|
||
func signingMethod(algorithm string) jwt.SigningMethod {
|
||
switch strings.ToUpper(algorithm) {
|
||
case "HS384":
|
||
return jwt.SigningMethodHS384
|
||
case "HS512":
|
||
return jwt.SigningMethodHS512
|
||
default:
|
||
return jwt.SigningMethodHS256
|
||
}
|
||
}
|
||
|
||
// ParseToken 解析 JWT Token
|
||
func ParseToken(tokenString string) (*Claims, error) {
|
||
cfg := config.Get()
|
||
|
||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
||
return []byte(cfg.JWT.Secret), nil
|
||
})
|
||
|
||
if err != nil {
|
||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||
return nil, ErrTokenExpired
|
||
}
|
||
if errors.Is(err, jwt.ErrTokenMalformed) {
|
||
return nil, ErrTokenMalformed
|
||
}
|
||
if errors.Is(err, jwt.ErrTokenNotValidYet) {
|
||
return nil, ErrTokenNotValidYet
|
||
}
|
||
return nil, ErrTokenInvalid
|
||
}
|
||
|
||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||
// 使用 JTI 检查黑名单(更高效)
|
||
if claims.JTI != "" && tokenBlacklist.IsBlacklisted(claims.JTI) {
|
||
return nil, ErrTokenRevoked
|
||
}
|
||
return claims, nil
|
||
}
|
||
|
||
return nil, ErrTokenInvalid
|
||
}
|
||
|
||
// InvalidateToken 使 Token 失效(加入黑名单)
|
||
func InvalidateToken(tokenString string) error {
|
||
cfg := config.Get()
|
||
|
||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
||
return []byte(cfg.JWT.Secret), nil
|
||
})
|
||
|
||
if err != nil {
|
||
// Token 无效或已过期,无需加入黑名单
|
||
return nil
|
||
}
|
||
|
||
if claims, ok := token.Claims.(*Claims); ok {
|
||
if claims.JTI != "" && claims.ExpiresAt != nil {
|
||
return tokenBlacklist.Add(claims.JTI, claims.ExpiresAt.Time)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// InvalidateTokenByID 直接通过 JTI 使 Token 失效
|
||
// 参数: jti JWT ID,expiry 过期时间
|
||
func InvalidateTokenByID(jti string, expiry time.Time) error {
|
||
return tokenBlacklist.Add(jti, expiry)
|
||
}
|
||
|
||
// RefreshToken 刷新 Token
|
||
func RefreshToken(tokenString string) (string, error) {
|
||
claims, err := ParseToken(tokenString)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
// 将旧 Token 加入黑名单
|
||
if claims.JTI != "" && claims.ExpiresAt != nil {
|
||
tokenBlacklist.Add(claims.JTI, claims.ExpiresAt.Time)
|
||
}
|
||
|
||
return GenerateToken(claims.UserID, claims.Username, claims.Role, claims.UserType)
|
||
}
|
||
|
||
// GetJTI 从 Token 中提取 JTI(不验证签名)
|
||
// 用于需要在验证前获取 JTI 的场景
|
||
func GetJTI(tokenString string) (string, error) {
|
||
token, _, err := jwt.NewParser().ParseUnverified(tokenString, &Claims{})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
if claims, ok := token.Claims.(*Claims); ok {
|
||
return claims.JTI, nil
|
||
}
|
||
|
||
return "", ErrTokenInvalid
|
||
}
|
||
|
||
// IsTokenRevoked 检查 Token 是否被撤销(通过 JTI)
|
||
func IsTokenRevoked(jti string) bool {
|
||
return tokenBlacklist.IsBlacklisted(jti)
|
||
}
|
||
|
||
// GetClaimsFromToken 获取 Token 的 Claims(不验证过期)
|
||
// 用于获取已过期 Token 的信息
|
||
func GetClaimsFromToken(tokenString string) (*Claims, error) {
|
||
cfg := config.Get()
|
||
|
||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
||
return []byte(cfg.JWT.Secret), nil
|
||
}, jwt.WithoutClaimsValidation())
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if claims, ok := token.Claims.(*Claims); ok {
|
||
return claims, nil
|
||
}
|
||
|
||
return nil, ErrTokenInvalid
|
||
} |