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
|
|||
|
|
}
|