backend/pkg/library/centrifugo/blacklist.go

131 lines
3.8 KiB
Go
Raw Normal View History

2026-01-06 07:15:18 +00:00
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 撤銷:使用 JTIJWT 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
}