diff --git a/docs/superpowers/specs/2026-05-14-redis-token-blacklist-design.md b/docs/superpowers/specs/2026-05-14-redis-token-blacklist-design.md new file mode 100644 index 0000000..780a628 --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-redis-token-blacklist-design.md @@ -0,0 +1,237 @@ +# 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:{tokenSignature}` | `{userID}:{banReason}` | 与 Token 过期时间一致 | + +**Value 结构说明:** +- `userID`: 被封禁用户的 ID +- `banReason`: 封禁原因(可选,便于后续追踪) + +**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" + "fmt" + "strconv" + "time" + + "github.com/redis/go-redis/v9" +) + +const ( + BlacklistKeyPrefix = "blacklist:token:" +) + +// BlacklistEntry 黑名单条目 +type BlacklistEntry struct { + UserID int64 + BanReason string +} + +// 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 RedisClient == nil { + return fmt.Errorf("redis client is not initialized") + } + + key := BlacklistKeyPrefix + token + value := fmt.Sprintf("%d:%s", userID, banReason) + + return RedisClient.Set(ctx, key, value, ttl).Err() +} + +// IsBlacklisted 检查 Token 是否在黑名单 +// 返回: (是否在黑名单, 用户ID, 封禁原因, error) +func IsBlacklisted(ctx context.Context, token string) (bool, int64, string, error) { + if RedisClient == nil { + return false, 0, "", fmt.Errorf("redis client is not initialized") + } + + key := BlacklistKeyPrefix + token + value, err := RedisClient.Get(ctx, key).Result() + if err == redis.Nil { + return false, 0, "", nil + } + if err != nil { + return false, 0, "", err + } + + // 解析 value: "userID:banReason" + var userID int64 + var banReason string + _, parseErr := fmt.Sscanf(value, "%d:%s", &userID, &banReason) + if parseErr != nil { + // 兼容旧格式或只存储 userID 的情况 + userID, _ = strconv.ParseInt(value, 10, 64) + banReason = "" + } + + return true, userID, banReason, nil +} + +// RemoveFromBlacklist 从黑名单移除 Token(用于解封) +func RemoveFromBlacklist(ctx context.Context, token string) error { + if RedisClient == nil { + return fmt.Errorf("redis client is not initialized") + } + + key := BlacklistKeyPrefix + 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 | 初始设计 | \ No newline at end of file