- 使用 SHA256 哈希代替原始 token 作为 Key - 使用 JSON 格式存储 value,避免解析问题 - 添加输入验证 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
256 lines
6.4 KiB
Markdown
256 lines
6.4 KiB
Markdown
# 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:{tokenHash}` | `{"user_id":123,"reason":"封禁原因"}` | 与 Token 剩余有效期一致 |
|
||
|
||
**Key 结构说明:**
|
||
- `tokenHash`: 使用 SHA256 对 Token 进行哈希,避免使用长字符串作为 Key
|
||
- 这样做既保证了唯一性,又节省 Redis 内存
|
||
|
||
**Value 结构说明:**
|
||
- JSON 格式存储,包含 `user_id` 和 `reason` 字段
|
||
- 使用 JSON 格式避免 value 中包含特殊字符导致的解析问题
|
||
|
||
**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"
|
||
"crypto/sha256"
|
||
"encoding/json"
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
)
|
||
|
||
const (
|
||
BlacklistKeyPrefix = "blacklist:token:"
|
||
)
|
||
|
||
// BlacklistEntry 黑名单条目
|
||
type BlacklistEntry struct {
|
||
UserID int64 `json:"user_id"`
|
||
Reason string `json:"reason"`
|
||
}
|
||
|
||
// tokenToHash 将 Token 转换为哈希作为 Key
|
||
func tokenToHash(token string) string {
|
||
hash := sha256.Sum256([]byte(token))
|
||
return fmt.Sprintf("%x", hash)
|
||
}
|
||
|
||
// 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 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 是否在黑名单
|
||
// 返回: (是否在黑名单, 用户ID, 封禁原因, error)
|
||
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()
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 三、配置项
|
||
|
||
### 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 | 初始设计 | |