package centrifugo import ( "context" "errors" "fmt" "time" "github.com/zeromicro/go-zero/core/stores/redis" ) // 錯誤定義 var ( ErrBlacklistNotConfigured = errors.New("token blacklist is not configured") ErrOnlineStoreNotConfigured = errors.New("online store is not configured") ) // TokenBlacklist Token 黑名單管理器 // 提供兩種撤銷機制: // 1. 單一 Token 撤銷:使用 JTI(JWT ID)將特定 Token 加入黑名單 // 2. 用戶全部撤銷:使用版本號機制,使用戶之前所有 Token 失效 type TokenBlacklist struct { redis *redis.Redis prefix string } // NewTokenBlacklist 創建 Token 黑名單管理器 func NewTokenBlacklist(redisClient *redis.Redis) *TokenBlacklist { return &TokenBlacklist{ redis: redisClient, prefix: "centrifugo:blacklist:", } } // NewTokenBlacklistWithPrefix 創建帶自定義前綴的 Token 黑名單管理器 func NewTokenBlacklistWithPrefix(redisClient *redis.Redis, prefix string) *TokenBlacklist { return &TokenBlacklist{ redis: redisClient, prefix: prefix, } } // ==================== 撤銷操作 ==================== // RevokeToken 撤銷特定 Token(使用 JTI) // ttl: 黑名單過期時間,應設置為 Token 的剩餘有效時間 // // 使用場景: // - 用戶登出單一設備 // - 檢測到可疑活動的特定 session func (b *TokenBlacklist) RevokeToken(ctx context.Context, jti string, ttl time.Duration) error { if jti == "" { return errors.New("jti cannot be empty") } key := b.tokenKey(jti) return b.redis.SetexCtx(ctx, key, "revoked", int(ttl.Seconds())) } // RevokeUserTokens 撤銷用戶的所有 Token(使用版本控制) // 通過更新版本號,使該用戶之前發出的所有 Token 失效 // // 使用場景: // - 用戶被封禁 // - 密碼變更 // - 用戶主動登出全部設備 func (b *TokenBlacklist) RevokeUserTokens(ctx context.Context, userID string) error { if userID == "" { return errors.New("userID cannot be empty") } key := b.userVersionKey(userID) version := time.Now().UnixNano() // 設置 7 天過期,足夠長於任何 Token 的有效期 return b.redis.SetexCtx(ctx, key, fmt.Sprintf("%d", version), 7*24*3600) } // ==================== 驗證操作 ==================== // IsTokenRevoked 檢查 Token 是否被撤銷(使用 JTI) func (b *TokenBlacklist) IsTokenRevoked(ctx context.Context, jti string) (bool, error) { if jti == "" { return false, nil } key := b.tokenKey(jti) exists, err := b.redis.ExistsCtx(ctx, key) if err != nil { return false, err } return exists, nil } // GetUserTokenVersion 獲取用戶的 Token 版本 // 返回 0 表示沒有設置版本(用戶從未被撤銷過) func (b *TokenBlacklist) GetUserTokenVersion(ctx context.Context, userID string) (int64, error) { if userID == "" { return 0, nil } key := b.userVersionKey(userID) val, err := b.redis.GetCtx(ctx, key) if err != nil { return 0, err } if val == "" { return 0, nil } var version int64 _, err = fmt.Sscanf(val, "%d", &version) return version, err } // IsTokenVersionValid 檢查 Token 版本是否有效 // tokenVersion: Token 內嵌的版本號 // 如果 currentVersion > tokenVersion,表示 Token 已被撤銷 func (b *TokenBlacklist) IsTokenVersionValid(ctx context.Context, userID string, tokenVersion int64) (bool, error) { currentVersion, err := b.GetUserTokenVersion(ctx, userID) if err != nil { return false, err } // 如果沒有設置版本,或 Token 版本 >= 當前版本,則有效 return currentVersion == 0 || tokenVersion >= currentVersion, nil } // ==================== Key 生成 ==================== func (b *TokenBlacklist) tokenKey(jti string) string { return b.prefix + "token:" + jti } func (b *TokenBlacklist) userVersionKey(userID string) string { return b.prefix + "user_version:" + userID }