diff --git a/docs/superpowers/plans/2026-05-14-redis-token-blacklist-implementation-plan.md b/docs/superpowers/plans/2026-05-14-redis-token-blacklist-implementation-plan.md new file mode 100644 index 0000000..505dcd9 --- /dev/null +++ b/docs/superpowers/plans/2026-05-14-redis-token-blacklist-implementation-plan.md @@ -0,0 +1,372 @@ +# Redis Token 黑名单实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 实现 JWT Token 黑名单功能,用于账号封禁和强制下线场景 + +**Architecture:** 在 gateway 层集成 Redis,JWT 中间件每次请求检查 Token 是否在黑名单。Redis Key 使用 SHA256 哈希存储,Value 使用 JSON 格式。 + +**Tech Stack:** Go, github.com/redis/go-redis/v9, gin middleware + +--- + +## 文件结构 + +``` +backend/pkg/database/ +├── database.go # 现有 PostgreSQL(GORM) +└── redis.go # 新建:Redis 客户端 + Token 黑名单 + +backend/gateway/config/config.go # 修改:添加 Redis 配置 +backend/gateway/middleware/ # 修改:JWT 中间件检查黑名单 +backend/gateway/main.go # 修改:初始化 Redis +``` + +--- + +### Task 1: 创建 Redis 客户端和 Token 黑名单模块 + +**Files:** +- Create: `backend/pkg/database/redis.go` + +- [ ] **Step 1: 创建 redis.go 文件** + +```go +package database + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "time" + + "github.com/redis/go-redis/v9" +) + +const ( + BlacklistKeyPrefix = "blacklist:token:" +) + +// RedisClient Redis 客户端单例 +var RedisClient *redis.Client + +// Config Redis 配置 +type RedisConfig struct { + Host string + Port int + Password string + DB int +} + +// InitRedis 初始化 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 +} + +// CloseRedis 关闭 Redis 连接 +func CloseRedis() error { + if RedisClient != nil { + return RedisClient.Close() + } + return nil +} + +// GetRedis 获取 Redis 客户端实例 +func GetRedis() *redis.Client { + return RedisClient +} + +// RedisHealthCheck 健康检查 +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() +} + +// BlacklistEntry 黑名单条目 +type BlacklistEntry struct { + UserID int64 `json:"user_id"` + Reason string `json:"reason"` +} + +// tokenToHash 将 Token 转换为 SHA256 哈希作为 Key +func tokenToHash(token string) string { + hash := sha256.Sum256([]byte(token)) + return fmt.Sprintf("%x", hash) +} + +// AddToBlacklist 添加 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 是否在黑名单 +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() +} +``` + +- [ ] **Step 2: 提交代码** + +```bash +git add backend/pkg/database/redis.go +git commit -m "feat: 添加 Redis 客户端和 Token 黑名单模块 + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 2: 添加 Redis 配置到 config.go + +**Files:** +- Modify: `backend/gateway/config/config.go` + +- [ ] **Step 1: 添加 RedisConfig 结构体到 Config 结构体** + +在 `Config` 结构体中添加 `Redis` 字段: + +```go +type Config struct { + Server ServerConfig + Dubbo DubboConfig + JWT JWTConfig + OSS OSSConfig + Redis RedisConfig // 新增 + Root string +} +``` + +- [ ] **Step 2: 添加 RedisConfig 结构体定义** + +在 `ServerConfig` 之前添加: + +```go +// RedisConfig Redis 配置 +type RedisConfig struct { + Host string + Port int + Password string + DB int +} +``` + +- [ ] **Step 3: 在 Load() 函数中添加 Redis 配置加载** + +在 return 语句的 OSS 配置后添加: + +```go +Redis: RedisConfig{ + Host: getEnv("REDIS_HOST", "127.0.0.1"), + Port: getEnvInt("REDIS_PORT", 6379), + Password: getEnv("REDIS_PASSWORD", ""), + DB: getEnvInt("REDIS_DB", 0), +}, +``` + +- [ ] **Step 4: 提交代码** + +```bash +git add backend/gateway/config/config.go +git commit -m "feat: 添加 Redis 配置项 + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 3: 在 main.go 中初始化 Redis + +**Files:** +- Modify: `backend/gateway/main.go` + +- [ ] **Step 1: 添加 database 包导入** + +在导入部分添加: + +```go +"github.com/topfans/backend/pkg/database" +``` + +- [ ] **Step 2: 在加载配置后初始化 Redis** + +在 `cfg.Validate()` 之后,初始化 Dubbo clients 之前添加: + +```go +// 3.5 初始化 Redis +logger.Logger.Info("Connecting to Redis...") +if err := database.InitRedis(database.RedisConfig{ + Host: cfg.Redis.Host, + Port: cfg.Redis.Port, + Password: cfg.Redis.Password, + DB: cfg.Redis.DB, +}); err != nil { + logger.Logger.Fatal("Failed to connect to Redis", zap.Error(err)) +} +logger.Logger.Info("Redis connected successfully") +defer database.CloseRedis() +``` + +- [ ] **Step 3: 提交代码** + +```bash +git add backend/gateway/main.go +git commit -m "feat: 初始化 Redis 连接 + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 4: 在 JWT 中间件中添加黑名单检查 + +**Files:** +- Modify: `backend/gateway/middleware/auth_middleware.go` + +- [ ] **Step 1: 添加 database 包导入** + +```go +"github.com/topfans/backend/pkg/database" +``` + +- [ ] **Step 2: 在 ParseToken 成功后、检查用户信息存入 gin.Context 之前添加黑名单检查** + +在 `// 4. 将用户信息存入 gin.Context` 之前添加: + +```go +// 4. 检查 Token 是否在黑名单 +isBlacklisted, bannedUserID, banReason, err := database.IsBlacklisted(c.Request.Context(), token) +if err != nil { + logger.Logger.Error("Failed to check blacklist", + zap.String("path", c.Request.URL.Path), + zap.Error(err), + ) + // Redis 错误时fail-open,允许请求继续(可根据安全策略调整) +} +if isBlacklisted { + logger.Logger.Warn("Token is blacklisted", + zap.Int64("banned_user_id", bannedUserID), + zap.String("ban_reason", banReason), + zap.String("path", c.Request.URL.Path), + ) + response.Unauthorized(c, "账号已被封禁") + c.Abort() + return +} +``` + +- [ ] **Step 3: 提交代码** + +```bash +git add backend/gateway/middleware/auth_middleware.go +git commit -m "feat: JWT 中间件添加 Token 黑名单检查 + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 5: 添加 Redis go.mod 依赖 + +**Files:** +- Modify: `backend/gateway/go.mod` + +- [ ] **Step 1: 添加 redis 依赖** + +```bash +cd backend/gateway && go get github.com/redis/go-redis/v9@latest +``` + +- [ ] **Step 2: 提交代码** + +```bash +git add backend/gateway/go.mod backend/gateway/go.sum +git commit -m "deps: 添加 redis go-redis/v9 依赖 + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +### Task 6: 验证构建 + +- [ ] **Step 1: 运行 go build 验证代码编译** + +```bash +cd backend/gateway && go build ./... +``` + +Expected: 编译成功,无错误 + +--- + +## 变更记录 + +| 日期 | 变更内容 | +|------|---------| +| 2026-05-14 | 初始实现计划 | \ No newline at end of file