topfans/docs/superpowers/specs/2026-05-14-redis-token-blacklist-design.md
zerosaturation c38ad5a654 docs: 优化 Redis Token 黑名单设计
- 使用 SHA256 哈希代替原始 token 作为 Key
- 使用 JSON 格式存储 value,避免解析问题
- 添加输入验证

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 16:21:42 +08:00

6.4 KiB
Raw Blame History

JWT Token 黑名单设计文档

创建日期: 2026-05-14 更新日期: 2026-05-14 项目: TopFans Redis Token 黑名单 服务: Gateway / 通用 状态: 设计中


一、设计目标

实现 JWT Token 黑名单功能,用于:

  1. 账号封禁时 - 管理员封禁用户Token 立即加入黑名单
  2. 强制下线时 - 用户在多个设备登录,需要让某个 token 失效

二、技术方案

2.1 技术选型

  • 客户端: github.com/redis/go-redis/v9
  • 连接信息: 通过环境变量配置

2.2 Redis Key 设计

Key 格式 Value TTL
blacklist:token:{tokenHash} {"user_id":123,"reason":"封禁原因"} 与 Token 剩余有效期一致

Key 结构说明:

  • tokenHash: 使用 SHA256 对 Token 进行哈希,避免使用长字符串作为 Key
  • 这样做既保证了唯一性,又节省 Redis 内存

Value 结构说明:

  • JSON 格式存储,包含 user_idreason 字段
  • 使用 JSON 格式避免 value 中包含特殊字符导致的解析问题

TTL 说明:

  • TTL 与 Token 剩余有效期一致Token 过期后自动清理黑名单记录

2.3 核心接口

package database

import (
    "context"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
)

// RedisClient Redis 客户端
var RedisClient *redis.Client

// Config Redis 配置
type RedisConfig struct {
    Host     string
    Port     int
    Password string
    DB       int
}

// Init 初始化 Redis 连接
func InitRedis(cfg RedisConfig) error {
    RedisClient = redis.NewClient(&redis.Options{
        Addr:     fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
        Password: cfg.Password,
        DB:       cfg.DB,
    })

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := RedisClient.Ping(ctx).Err(); err != nil {
        return fmt.Errorf("failed to connect to redis: %w", err)
    }

    return nil
}

// Close 关闭 Redis 连接
func CloseRedis() error {
    if RedisClient != nil {
        return RedisClient.Close()
    }
    return nil
}

// GetRedis 获取 Redis 客户端实例
func GetRedis() *redis.Client {
    return RedisClient
}

// HealthCheck 健康检查
func RedisHealthCheck() error {
    if RedisClient == nil {
        return fmt.Errorf("redis client is not initialized")
    }
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    return RedisClient.Ping(ctx).Err()
}

2.4 Token 黑名单操作

package database

import (
    "context"
    "crypto/sha256"
    "encoding/json"
    "fmt"
    "time"

    "github.com/redis/go-redis/v9"
)

const (
    BlacklistKeyPrefix = "blacklist:token:"
)

// BlacklistEntry 黑名单条目
type BlacklistEntry struct {
    UserID  int64  `json:"user_id"`
    Reason  string `json:"reason"`
}

// tokenToHash 将 Token 转换为哈希作为 Key
func tokenToHash(token string) string {
    hash := sha256.Sum256([]byte(token))
    return fmt.Sprintf("%x", hash)
}

// AddToBlacklist 添加 Token 到黑名单
// token: JWT Token 字符串
// userID: 用户 ID
// banReason: 封禁原因
// ttl: Token 剩余有效期
func AddToBlacklist(ctx context.Context, token string, userID int64, banReason string, ttl time.Duration) error {
    if token == "" {
        return fmt.Errorf("token is empty")
    }
    if RedisClient == nil {
        return fmt.Errorf("redis client is not initialized")
    }

    key := BlacklistKeyPrefix + tokenToHash(token)
    entry := BlacklistEntry{UserID: userID, Reason: banReason}
    value, err := json.Marshal(entry)
    if err != nil {
        return fmt.Errorf("failed to marshal blacklist entry: %w", err)
    }

    return RedisClient.Set(ctx, key, value, ttl).Err()
}

// IsBlacklisted 检查 Token 是否在黑名单
// 返回: (是否在黑名单, 用户ID, 封禁原因, error)
func IsBlacklisted(ctx context.Context, token string) (bool, int64, string, error) {
    if token == "" {
        return false, 0, "", nil
    }
    if RedisClient == nil {
        return false, 0, "", fmt.Errorf("redis client is not initialized")
    }

    key := BlacklistKeyPrefix + tokenToHash(token)
    value, err := RedisClient.Get(ctx, key).Result()
    if err == redis.Nil {
        return false, 0, "", nil
    }
    if err != nil {
        return false, 0, "", err
    }

    var entry BlacklistEntry
    if err := json.Unmarshal([]byte(value), &entry); err != nil {
        return false, 0, "", fmt.Errorf("failed to unmarshal blacklist entry: %w", err)
    }

    return true, entry.UserID, entry.Reason, nil
}

// RemoveFromBlacklist 从黑名单移除 Token用于解封
func RemoveFromBlacklist(ctx context.Context, token string) error {
    if token == "" {
        return nil
    }
    if RedisClient == nil {
        return fmt.Errorf("redis client is not initialized")
    }

    key := BlacklistKeyPrefix + tokenToHash(token)
    return RedisClient.Del(ctx, key).Err()
}

三、配置项

3.1 环境变量

变量名 说明 默认值
REDIS_HOST Redis 主机地址 127.0.0.1
REDIS_PORT Redis 端口 6379
REDIS_PASSWORD Redis 密码 (空)
REDIS_DB Redis 数据库编号 0

四、调用位置

场景 位置 触发时机
账号封禁 管理员操作 → 更新账号状态 → 调用 AddToBlacklist 封禁时
强制下线 用户操作 → 调用 AddToBlacklist 触发下线时
网关校验 JWT 校验层 → 调用 IsBlacklisted 每次请求
账号解封 管理员操作 → 调用 RemoveFromBlacklist 解封时

五、文件结构

backend/pkg/database/
├── database.go       # 现有 PostgreSQL
└── redis.go          # 新建Redis 客户端 + Token 黑名单

backend/gateway/config/config.go  # 修改:新增 Redis 配置

六、实现步骤

  1. 新建 pkg/database/redis.go,实现 Redis 客户端初始化 + 黑名单操作
  2. 修改 gateway/config/config.go,添加 Redis 配置项和环境变量读取
  3. 在 gateway 初始化时调用 database.InitRedis
  4. 在 JWT 中间件中调用 database.IsBlacklisted 检查黑名单
  5. 在账号封禁/强制下线时调用 database.AddToBlacklist

七、变更记录

日期 变更内容
2026-05-14 初始设计