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