topfans/docs/superpowers/plans/2026-05-14-redis-token-blacklist-implementation-plan.md
zerosaturation a7249110fe docs: 添加 Redis Token 黑名单实现计划
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 16:41:38 +08:00

8.8 KiB
Raw Blame History

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 层集成 RedisJWT 中间件每次请求检查 Token 是否在黑名单。Redis Key 使用 SHA256 哈希存储Value 使用 JSON 格式。

Tech Stack: Go, github.com/redis/go-redis/v9, gin middleware


文件结构

backend/pkg/database/
├── database.go       # 现有 PostgreSQLGORM
└── 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 文件

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: 提交代码
git add backend/pkg/database/redis.go
git commit -m "feat: 添加 Redis 客户端和 Token 黑名单模块

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 2: 添加 Redis 配置到 config.go

Files:

  • Modify: backend/gateway/config/config.go

  • Step 1: 添加 RedisConfig 结构体到 Config 结构体

Config 结构体中添加 Redis 字段:

type Config struct {
    Server ServerConfig
    Dubbo  DubboConfig
    JWT    JWTConfig
    OSS    OSSConfig
    Redis  RedisConfig  // 新增
    Root   string
}
  • Step 2: 添加 RedisConfig 结构体定义

ServerConfig 之前添加:

// RedisConfig Redis 配置
type RedisConfig struct {
    Host     string
    Port     int
    Password string
    DB       int
}
  • Step 3: 在 Load() 函数中添加 Redis 配置加载

在 return 语句的 OSS 配置后添加:

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: 提交代码
git add backend/gateway/config/config.go
git commit -m "feat: 添加 Redis 配置项

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 3: 在 main.go 中初始化 Redis

Files:

  • Modify: backend/gateway/main.go

  • Step 1: 添加 database 包导入

在导入部分添加:

"github.com/topfans/backend/pkg/database"
  • Step 2: 在加载配置后初始化 Redis

cfg.Validate() 之后,初始化 Dubbo clients 之前添加:

// 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: 提交代码
git add backend/gateway/main.go
git commit -m "feat: 初始化 Redis 连接

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 4: 在 JWT 中间件中添加黑名单检查

Files:

  • Modify: backend/gateway/middleware/auth_middleware.go

  • Step 1: 添加 database 包导入

"github.com/topfans/backend/pkg/database"
  • Step 2: 在 ParseToken 成功后、检查用户信息存入 gin.Context 之前添加黑名单检查

// 4. 将用户信息存入 gin.Context 之前添加:

// 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: 提交代码
git add backend/gateway/middleware/auth_middleware.go
git commit -m "feat: JWT 中间件添加 Token 黑名单检查

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"

Task 5: 添加 Redis go.mod 依赖

Files:

  • Modify: backend/gateway/go.mod

  • Step 1: 添加 redis 依赖

cd backend/gateway && go get github.com/redis/go-redis/v9@latest
  • Step 2: 提交代码
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 <noreply@anthropic.com>"

Task 6: 验证构建

  • Step 1: 运行 go build 验证代码编译
cd backend/gateway && go build ./...

Expected: 编译成功,无错误


变更记录

日期 变更内容
2026-05-14 初始实现计划