Files
smartmate/backend/auth/jwt_handler.go
LoveLosita 5038ec2fc5 Version:0.0.2.dev.260203
feat: implement redis-based logout and jwt middleware 🚀 feat: 实现基于 Redis 的登出机制与 JWT 中间件 🚀

Middleware Construction: Implemented JWTTokenAuth middleware for Gin, featuring structured claims parsing and active session validation. 🛡️
中间件构建:为 Gin 框架实现了 JWTTokenAuth 中间件,支持结构化 Claims 解析与活跃会话验证。🛡️

Redis Integration: Introduced Redis for high-performance state management. Integrated CacheDAO into the Dependency Injection (DI) chain. 
Redis 引入:引入 Redis 进行高性能状态管理。将 CacheDAO 成功集成至依赖注入 (DI) 调用链中。

Secure Logout Module: Developed the logout functional module using a Redis Blacklist mechanism. 🔐
安全登出模块:开发了基于 Redis 黑名单 机制的登出功能模块。🔐

Marked invalidated tokens by storing jti (JWT ID) in Redis with automatic TTL expiration.
通过在 Redis 中存储 jti(JWT 唯一标识)并设置自动 TTL 过期,实现 Token 的主动失效。

Added blacklist checkpoints in both AuthMiddleware and RefreshToken logic to prevent session resurrection.
在认证中间件与 Token 刷新逻辑中同步增设黑名单检查点,杜绝登出后的“死灰复燃”。

Architecture Refinement: Upgraded ValidateRefreshToken and Service-layer handlers to use type-safe struct assertions instead of raw MapClaims. 🏗️
架构精进:升级了 ValidateRefreshToken 与 Service 层处理器,改用类型安全的结构体断言取代原始的 MapClaims,提升了代码健壮性。🏗️
2026-02-03 16:53:16 +08:00

122 lines
4.0 KiB
Go

package auth
import (
"errors"
"time"
"github.com/LoveLosita/smartflow/backend/dao"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/respond"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/spf13/viper"
)
var RefreshKey = []byte(viper.GetString("jwt.refreshSecret")) // 用于签名和验证刷新Token的密钥
var AccessKey = []byte(viper.GetString("jwt.accessSecret")) // 用于签名和验证访问Token的密钥
// generateJTI 生成唯一的 JWT ID
func generateJTI() string {
return uuid.New().String()
}
// GenerateTokens 生成访问令牌和刷新令牌
func GenerateTokens(userID int) (string, string, error) {
// 创建访问令牌
sid := generateJTI()
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID, // 获取用户ID
"exp": time.Now().Add(15 * time.Minute).Unix(), // 设置访问令牌过期时间为 15 分钟
"token_type": "access_token", // 令牌类型为访问令牌
"jti": sid, // 亲子共用的 JWT ID
})
// 使用密钥签名访问令牌
accessTokenString, err := accessToken.SignedString(AccessKey)
if err != nil {
return "", "", err
}
// 创建刷新令牌
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID, // 获取用户ID
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(), // 设置刷新令牌过期时间为 7 天
"token_type": "refresh_token", // 令牌类型为刷新令牌
"jti": sid, // 亲子共用的 JWT ID
})
// 使用密钥签名刷新令牌
refreshTokenString, err := refreshToken.SignedString(RefreshKey)
if err != nil {
return "", "", err
}
return accessTokenString, refreshTokenString, nil
}
// ValidateRefreshToken 验证刷新令牌的有效性
/*func ValidateRefreshToken(tokenString string) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 检查签名方法是否为 HMAC
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, respond.InvalidTokenSingingMethod
}
// 返回用于验证的密钥
return RefreshKey, nil
})
if err != nil {
return nil, err
}
// 进一步检查载荷中 token_type 是否正确
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, respond.InvalidClaims
}
// 检查 token_type 是否是 refresh_token
if claimType, ok := claims["token_type"].(string); !ok || claimType != "refresh_token" {
return nil, respond.WrongTokenType
}
return token, nil
}
*/
// ValidateRefreshToken 验证刷新令牌的有效性,并增加 Redis 黑名单检查
func ValidateRefreshToken(tokenString string, cache *dao.CacheDAO) (*jwt.Token, error) {
// 1. 解析 Token 并直接绑定到你的自定义结构体
token, err := jwt.ParseWithClaims(tokenString, &model.MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, respond.InvalidTokenSingingMethod
}
return RefreshKey, nil
})
if err != nil || !token.Valid {
return nil, err
}
// 2. 断言获取 Claims
claims, ok := token.Claims.(*model.MyCustomClaims)
if !ok {
return nil, respond.InvalidClaims
}
// 3. 核心“设卡”:检查 token_type 是否是 refresh_token
if claims.TokenType != "refresh_token" {
return nil, respond.WrongTokenType
}
// 4. --- 🛡️ 终极关卡:检查 Redis 黑名单 ---
// 即使签名没过期,如果 jti 在黑名单里(用户已登出),也视为无效
isBlack, err := cache.IsBlacklisted(claims.Jti)
if err != nil {
// Redis 出错时的处理逻辑,建议报错以防“漏网之鱼”
return nil, respond.InternalError(errors.New("无法验证令牌状态"))
}
if isBlack {
return nil, respond.UserLoggedOut // 返回你定义的“用户已登出”错误
}
return token, nil
}