docs: 添加 JWT Token 黑名单设计文档

设计 Redis Token 黑名单功能,用于账号封禁和强制下线场景。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zerosaturation 2026-05-14 16:07:20 +08:00
parent e500bff6ff
commit 00a39e1819

View File

@ -0,0 +1,237 @@
# JWT Token 黑名单设计文档
> **创建日期:** 2026-05-14
> **更新日期:** 2026-05-14
> **项目:** TopFans Redis Token 黑名单
> **服务:** Gateway / 通用
> **状态:** 设计中
---
## 一、设计目标
实现 JWT Token 黑名单功能,用于:
1. **账号封禁时** - 管理员封禁用户Token 立即加入黑名单
2. **强制下线时** - 用户在多个设备登录,需要让某个 token 失效
---
## 二、技术方案
### 2.1 技术选型
- **客户端**: `github.com/redis/go-redis/v9`
- **连接信息**: 通过环境变量配置
### 2.2 Redis Key 设计
| Key 格式 | Value | TTL |
|---------|-------|-----|
| `blacklist:token:{tokenSignature}` | `{userID}:{banReason}` | 与 Token 过期时间一致 |
**Value 结构说明:**
- `userID`: 被封禁用户的 ID
- `banReason`: 封禁原因(可选,便于后续追踪)
**TTL 说明:**
- TTL 与 Token 过期时间一致Token 过期后自动清理黑名单记录
### 2.3 核心接口
```go
package database
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
// RedisClient Redis 客户端
var RedisClient *redis.Client
// Config Redis 配置
type RedisConfig struct {
Host string
Port int
Password string
DB int
}
// Init 初始化 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
}
// Close 关闭 Redis 连接
func CloseRedis() error {
if RedisClient != nil {
return RedisClient.Close()
}
return nil
}
// GetRedis 获取 Redis 客户端实例
func GetRedis() *redis.Client {
return RedisClient
}
// HealthCheck 健康检查
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()
}
```
### 2.4 Token 黑名单操作
```go
package database
import (
"context"
"fmt"
"strconv"
"time"
"github.com/redis/go-redis/v9"
)
const (
BlacklistKeyPrefix = "blacklist:token:"
)
// BlacklistEntry 黑名单条目
type BlacklistEntry struct {
UserID int64
BanReason string
}
// AddToBlacklist 添加 Token 到黑名单
// token: JWT Token 字符串
// userID: 用户 ID
// banReason: 封禁原因
// ttl: Token 剩余有效期(秒)
func AddToBlacklist(ctx context.Context, token string, userID int64, banReason string, ttl time.Duration) error {
if RedisClient == nil {
return fmt.Errorf("redis client is not initialized")
}
key := BlacklistKeyPrefix + token
value := fmt.Sprintf("%d:%s", userID, banReason)
return RedisClient.Set(ctx, key, value, ttl).Err()
}
// IsBlacklisted 检查 Token 是否在黑名单
// 返回: (是否在黑名单, 用户ID, 封禁原因, error)
func IsBlacklisted(ctx context.Context, token string) (bool, int64, string, error) {
if RedisClient == nil {
return false, 0, "", fmt.Errorf("redis client is not initialized")
}
key := BlacklistKeyPrefix + token
value, err := RedisClient.Get(ctx, key).Result()
if err == redis.Nil {
return false, 0, "", nil
}
if err != nil {
return false, 0, "", err
}
// 解析 value: "userID:banReason"
var userID int64
var banReason string
_, parseErr := fmt.Sscanf(value, "%d:%s", &userID, &banReason)
if parseErr != nil {
// 兼容旧格式或只存储 userID 的情况
userID, _ = strconv.ParseInt(value, 10, 64)
banReason = ""
}
return true, userID, banReason, nil
}
// RemoveFromBlacklist 从黑名单移除 Token用于解封
func RemoveFromBlacklist(ctx context.Context, token string) error {
if RedisClient == nil {
return fmt.Errorf("redis client is not initialized")
}
key := BlacklistKeyPrefix + token
return RedisClient.Del(ctx, key).Err()
}
```
---
## 三、配置项
### 3.1 环境变量
| 变量名 | 说明 | 默认值 |
|--------|------|-------|
| `REDIS_HOST` | Redis 主机地址 | 127.0.0.1 |
| `REDIS_PORT` | Redis 端口 | 6379 |
| `REDIS_PASSWORD` | Redis 密码 | (空) |
| `REDIS_DB` | Redis 数据库编号 | 0 |
---
## 四、调用位置
| 场景 | 位置 | 触发时机 |
|------|------|---------|
| **账号封禁** | 管理员操作 → 更新账号状态 → **调用 AddToBlacklist** | 封禁时 |
| **强制下线** | 用户操作 → **调用 AddToBlacklist** | 触发下线时 |
| **网关校验** | JWT 校验层 → **调用 IsBlacklisted** | 每次请求 |
| **账号解封** | 管理员操作 → **调用 RemoveFromBlacklist** | 解封时 |
---
## 五、文件结构
```
backend/pkg/database/
├── database.go # 现有 PostgreSQL
└── redis.go # 新建Redis 客户端 + Token 黑名单
backend/gateway/config/config.go # 修改:新增 Redis 配置
```
---
## 六、实现步骤
1. 新建 `pkg/database/redis.go`,实现 Redis 客户端初始化 + 黑名单操作
2. 修改 `gateway/config/config.go`,添加 Redis 配置项和环境变量读取
3. 在 gateway 初始化时调用 `database.InitRedis`
4. 在 JWT 中间件中调用 `database.IsBlacklisted` 检查黑名单
5. 在账号封禁/强制下线时调用 `database.AddToBlacklist`
---
## 七、变更记录
| 日期 | 变更内容 |
|------|---------|
| 2026-05-14 | 初始设计 |