topfans/docs/superpowers/plans/2026-05-14-redis-token-blacklist-implementation-plan.md
zerosaturation 17674e3d54 docs: 修复实现计划中的问题
- 修改 Redis 错误时 fail-closed(安全策略)
- 添加 Task 7 说明后续封禁 API 的扩展方向
- 添加 go.mod replace 说明

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 16:45:31 +08:00

418 lines
9.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
```
> **注意:** 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 文件**
```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 {
// 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: 提交代码**
```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: 编译成功,无错误
---
### 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"
}
```
**调用示例:**
```go
// 计算 Token 剩余 TTL
ttl := jwt.GetExpiresIn() * time.Second
// 添加到黑名单
err := database.AddToBlacklist(ctx, token, userID, reason, ttl)
```
---
## 变更记录
| 日期 | 变更内容 |
|------|---------|
| 2026-05-14 | 初始实现计划 |