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

372 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 文件**
```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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
### 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 <noreply@anthropic.com>"
```
---
### Task 6: 验证构建
- [ ] **Step 1: 运行 go build 验证代码编译**
```bash
cd backend/gateway && go build ./...
```
Expected: 编译成功,无错误
---
## 变更记录
| 日期 | 变更内容 |
|------|---------|
| 2026-05-14 | 初始实现计划 |