backend/pkg/library/centrifugo/blacklist.go

131 lines
3.8 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 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
}