- 修改 Redis 错误时 fail-closed(安全策略) - 添加 Task 7 说明后续封禁 API 的扩展方向 - 添加 go.mod replace 说明 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.8 KiB
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
注意: gateway 的 go.mod 使用
replace github.com/topfans/backend => ../,因此github.com/topfans/backend/pkg/database映射到backend/pkg/database/。
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 {
// Redis 错误时 fail-closed(安全策略),拒绝请求
logger.Logger.Error("Failed to check blacklist, rejecting request for security",
zap.String("path", c.Request.URL.Path),
zap.Error(err),
)
response.Unauthorized(c, "认证服务异常,请稍后重试")
c.Abort()
return
}
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: 编译成功,无错误
Task 7: 添加封禁用户的 Admin API(后续扩展)
说明: 此任务为后续扩展任务,本次实现暂不包含。Token 黑名单的基础设施已建立,后续需要封禁用户时调用
database.AddToBlacklist即可。
Files:
- Modify:
backend/gateway/controller/admin_controller.go(新建或修改) - Modify:
backend/gateway/router/router.go
API 设计:
POST /api/v1/admin/ban
Authorization: Bearer {admin_token}
Content-Type: application/json
Request:
{
"user_id": 123,
"token": "user_jwt_token_to_ban",
"reason": "违规发言"
}
Response:
{
"code": 200,
"message": "ok"
}
调用示例:
// 计算 Token 剩余 TTL
ttl := jwt.GetExpiresIn() * time.Second
// 添加到黑名单
err := database.AddToBlacklist(ctx, token, userID, reason, ttl)
变更记录
| 日期 | 变更内容 |
|---|---|
| 2026-05-14 | 初始实现计划 |