docs: 优化 Redis Token 黑名单设计

- 使用 SHA256 哈希代替原始 token 作为 Key
- 使用 JSON 格式存储 value,避免解析问题
- 添加输入验证

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

View File

@ -27,14 +27,18 @@
| Key 格式 | Value | TTL | | Key 格式 | Value | TTL |
|---------|-------|-----| |---------|-------|-----|
| `blacklist:token:{tokenSignature}` | `{userID}:{banReason}` | 与 Token 过期时间一致 | | `blacklist:token:{tokenHash}` | `{"user_id":123,"reason":"封禁原因"}` | 与 Token 剩余有效期一致 |
**Key 结构说明:**
- `tokenHash`: 使用 SHA256 对 Token 进行哈希,避免使用长字符串作为 Key
- 这样做既保证了唯一性,又节省 Redis 内存
**Value 结构说明:** **Value 结构说明:**
- `userID`: 被封禁用户的 ID - JSON 格式存储,包含 `user_id``reason` 字段
- `banReason`: 封禁原因(可选,便于后续追踪) - 使用 JSON 格式避免 value 中包含特殊字符导致的解析问题
**TTL 说明:** **TTL 说明:**
- TTL 与 Token 过期时间一致Token 过期后自动清理黑名单记录 - TTL 与 Token 剩余有效期一致Token 过期后自动清理黑名单记录
### 2.3 核心接口 ### 2.3 核心接口
@ -109,8 +113,9 @@ package database
import ( import (
"context" "context"
"crypto/sha256"
"encoding/json"
"fmt" "fmt"
"strconv"
"time" "time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
@ -122,22 +127,35 @@ const (
// BlacklistEntry 黑名单条目 // BlacklistEntry 黑名单条目
type BlacklistEntry struct { type BlacklistEntry struct {
UserID int64 UserID int64 `json:"user_id"`
BanReason string Reason string `json:"reason"`
}
// tokenToHash 将 Token 转换为哈希作为 Key
func tokenToHash(token string) string {
hash := sha256.Sum256([]byte(token))
return fmt.Sprintf("%x", hash)
} }
// AddToBlacklist 添加 Token 到黑名单 // AddToBlacklist 添加 Token 到黑名单
// token: JWT Token 字符串 // token: JWT Token 字符串
// userID: 用户 ID // userID: 用户 ID
// banReason: 封禁原因 // banReason: 封禁原因
// ttl: Token 剩余有效期(秒) // ttl: Token 剩余有效期
func AddToBlacklist(ctx context.Context, token string, userID int64, banReason string, ttl time.Duration) error { 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 { if RedisClient == nil {
return fmt.Errorf("redis client is not initialized") return fmt.Errorf("redis client is not initialized")
} }
key := BlacklistKeyPrefix + token key := BlacklistKeyPrefix + tokenToHash(token)
value := fmt.Sprintf("%d:%s", userID, banReason) 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() return RedisClient.Set(ctx, key, value, ttl).Err()
} }
@ -145,11 +163,14 @@ func AddToBlacklist(ctx context.Context, token string, userID int64, banReason s
// IsBlacklisted 检查 Token 是否在黑名单 // IsBlacklisted 检查 Token 是否在黑名单
// 返回: (是否在黑名单, 用户ID, 封禁原因, error) // 返回: (是否在黑名单, 用户ID, 封禁原因, error)
func IsBlacklisted(ctx context.Context, token string) (bool, int64, string, error) { func IsBlacklisted(ctx context.Context, token string) (bool, int64, string, error) {
if token == "" {
return false, 0, "", nil
}
if RedisClient == nil { if RedisClient == nil {
return false, 0, "", fmt.Errorf("redis client is not initialized") return false, 0, "", fmt.Errorf("redis client is not initialized")
} }
key := BlacklistKeyPrefix + token key := BlacklistKeyPrefix + tokenToHash(token)
value, err := RedisClient.Get(ctx, key).Result() value, err := RedisClient.Get(ctx, key).Result()
if err == redis.Nil { if err == redis.Nil {
return false, 0, "", nil return false, 0, "", nil
@ -158,26 +179,24 @@ func IsBlacklisted(ctx context.Context, token string) (bool, int64, string, erro
return false, 0, "", err return false, 0, "", err
} }
// 解析 value: "userID:banReason" var entry BlacklistEntry
var userID int64 if err := json.Unmarshal([]byte(value), &entry); err != nil {
var banReason string return false, 0, "", fmt.Errorf("failed to unmarshal blacklist entry: %w", err)
_, parseErr := fmt.Sscanf(value, "%d:%s", &userID, &banReason)
if parseErr != nil {
// 兼容旧格式或只存储 userID 的情况
userID, _ = strconv.ParseInt(value, 10, 64)
banReason = ""
} }
return true, userID, banReason, nil return true, entry.UserID, entry.Reason, nil
} }
// RemoveFromBlacklist 从黑名单移除 Token用于解封 // RemoveFromBlacklist 从黑名单移除 Token用于解封
func RemoveFromBlacklist(ctx context.Context, token string) error { func RemoveFromBlacklist(ctx context.Context, token string) error {
if token == "" {
return nil
}
if RedisClient == nil { if RedisClient == nil {
return fmt.Errorf("redis client is not initialized") return fmt.Errorf("redis client is not initialized")
} }
key := BlacklistKeyPrefix + token key := BlacklistKeyPrefix + tokenToHash(token)
return RedisClient.Del(ctx, key).Err() return RedisClient.Del(ctx, key).Err()
} }
``` ```