docs: 添加 Redis Token 黑名单实现计划
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c38ad5a654
commit
a7249110fe
@ -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 <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 | 初始实现计划 |
|
||||||
Loading…
Reference in New Issue
Block a user