From c38ad5a654a398459259bc85ba1f4b79b052c789 Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Thu, 14 May 2026 16:21:42 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BC=98=E5=8C=96=20Redis=20Token=20?= =?UTF-8?q?=E9=BB=91=E5=90=8D=E5=8D=95=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 SHA256 哈希代替原始 token 作为 Key - 使用 JSON 格式存储 value,避免解析问题 - 添加输入验证 Co-Authored-By: Claude Opus 4.6 --- ...2026-05-14-redis-token-blacklist-design.md | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/docs/superpowers/specs/2026-05-14-redis-token-blacklist-design.md b/docs/superpowers/specs/2026-05-14-redis-token-blacklist-design.md index 780a628..1112214 100644 --- a/docs/superpowers/specs/2026-05-14-redis-token-blacklist-design.md +++ b/docs/superpowers/specs/2026-05-14-redis-token-blacklist-design.md @@ -27,14 +27,18 @@ | Key 格式 | Value | TTL | |---------|-------|-----| -| `blacklist:token:{tokenSignature}` | `{userID}:{banReason}` | 与 Token 过期时间一致 | +| `blacklist:token:{tokenHash}` | `{"user_id":123,"reason":"封禁原因"}` | 与 Token 剩余有效期一致 | + +**Key 结构说明:** +- `tokenHash`: 使用 SHA256 对 Token 进行哈希,避免使用长字符串作为 Key +- 这样做既保证了唯一性,又节省 Redis 内存 **Value 结构说明:** -- `userID`: 被封禁用户的 ID -- `banReason`: 封禁原因(可选,便于后续追踪) +- JSON 格式存储,包含 `user_id` 和 `reason` 字段 +- 使用 JSON 格式避免 value 中包含特殊字符导致的解析问题 **TTL 说明:** -- TTL 与 Token 过期时间一致,Token 过期后自动清理黑名单记录 +- TTL 与 Token 剩余有效期一致,Token 过期后自动清理黑名单记录 ### 2.3 核心接口 @@ -109,8 +113,9 @@ package database import ( "context" + "crypto/sha256" + "encoding/json" "fmt" - "strconv" "time" "github.com/redis/go-redis/v9" @@ -122,22 +127,35 @@ const ( // BlacklistEntry 黑名单条目 type BlacklistEntry struct { - UserID int64 - BanReason string + 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 剩余有效期(秒) +// 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 + token - value := fmt.Sprintf("%d:%s", userID, banReason) + 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() } @@ -145,11 +163,14 @@ func AddToBlacklist(ctx context.Context, token string, userID int64, banReason s // 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 + token + key := BlacklistKeyPrefix + tokenToHash(token) value, err := RedisClient.Get(ctx, key).Result() if err == redis.Nil { return false, 0, "", nil @@ -158,26 +179,24 @@ func IsBlacklisted(ctx context.Context, token string) (bool, int64, string, erro 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 = "" + 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, userID, banReason, nil + 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 + token + key := BlacklistKeyPrefix + tokenToHash(token) return RedisClient.Del(ctx, key).Err() } ```