Files
xlgo-core/jwt/jwt.go
T
杭州明婳科技 dcfd24b624 release: v1.0.3 bug fix release
v1.0.3 定位为 bug fix release,收口 v1.0.2 引入的破坏性清理并修复
4 个轻量 bug + 依赖复查 + 版本号治理 + 文档对齐。同时包含此前未提交
的 v1.0.2 全部工作。

v1.0.3 变更:

Fixed:
- generateJTI 忽略 rand.Read 错误(jwt)— 改为 (string, error) 并传播
- QueryBuilder.Page Count 受残留 Limit 截断(repository)— countDB 加 Limit(-1).Offset(-1)
- OSS/本地存储文件名冲突(storage)— 新增 uniqueFilename 加 8 字节随机后缀
- 数据库重试策略对不可恢复错误无效(database)— 新增 isTransientDBError

Dependencies:
- go mod tidy 补全 postgres 方言传递依赖;安全补丁升级
  x/crypto v0.49→v0.53、golang-jwt/jwt/v5 v5.2.1→v5.3.1、gorilla/websocket v1.5.1→v1.5.3

Removed:
- Breaking: 清理 v1.0.2 兼容别名(InitMySQL* / driverName)
- 死代码 database.DBResolver
- 代码中 292 行"评分/理由"自夸注释(#26,23 个文件)

Changed:
- Breaking: 错误码体系重构 CodeSuccess 1→0、CodeFail 0→1,删除 CodeInvalidParams,加编译期防撞码
- database/mysql.go → manager.go;Logger 拆分三独立 core 修复 Tee 重复写入
- 版本号常量化:app.go 新增 const Version 作为唯一来源,CLI/脚手架模板引用之,不再散落字面量

Security:
- CORS 中间件按 W3C 规范修复 Allow-Credentials / Vary: Origin / 非白名单不回显

Added:
- console 包显式 level 控制(SetLevel/WithLevel/LevelSilent,atomic.Int32 并发安全)
- 新增测试 jwt/repository/storage/database/router/middleware/app
- 文档:新增 CHANGELOG.md、Version_v1.0.2_report.md;更新 README/GUIDE 顶部更新日志
- 文档对齐:删除对不存在的 config.DefaultManager() getter 的描述(保持 API 与文档一致)

详见 CHANGELOG.md 与 README.md 更新日志。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:40:10 +08:00

256 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package jwt
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"time"
"github.com/EthanCodeCraft/xlgo-core/config"
"github.com/EthanCodeCraft/xlgo-core/database"
"github.com/golang-jwt/jwt/v5"
)
// 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 优化)
type TokenBlacklist struct{}
// Add 将 Token 的 JTI 加入黑名单
// 参数: jti JWT IDexpiry Token 过期时间
func (tb *TokenBlacklist) Add(jti string, expiry time.Time) error {
if database.RedisClient == 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 database.RedisClient.Set(ctx, key, "1", ttl).Err()
}
// IsBlacklisted 检查 JTI 是否在黑名单中
func (tb *TokenBlacklist) IsBlacklisted(jti string) bool {
if database.RedisClient == nil {
// Redis 未启用,不检查黑名单
return false
}
ctx := context.Background()
key := fmt.Sprintf("jwt_bl:%s", jti)
return database.RedisClient.Exists(ctx, key).Val() > 0
}
// 全局黑名单实例
var tokenBlacklist = &TokenBlacklist{}
// 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(time.Duration(cfg.JWT.Expire) * time.Second)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "xlgo",
ID: jti, // 同时设置到 RegisteredClaims.ID
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, 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: "xlgo",
ID: jti,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(cfg.JWT.Secret))
}
// 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 IDexpiry 过期时间
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
}