# 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_id` 和 `reason` 字段 - 使用 JSON 格式避免 value 中包含特殊字符导致的解析问题 **TTL 说明:** - TTL 与 Token 剩余有效期一致,Token 过期后自动清理黑名单记录 ### 2.3 核心接口 ```go 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 黑名单操作 ```go 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 | 初始设计 |