Add Redis-based one-time download token support

Integrate Redis as a backend for one-time download token validation, with in-memory fallback. Added RedisManager for connection lifecycle, TokenStore for atomic token usage checks, and related configuration via environment variables. Updated download flow to ensure tokens are single-use, and improved API robustness for batch operations. Updated dependencies to include redis and cachetools.
This commit is contained in:
2025-12-26 18:47:46 +08:00
parent 3088a9d548
commit d8a229fccd
13 changed files with 302 additions and 15 deletions

View File

@@ -0,0 +1,65 @@
"""
一次性令牌状态存储
支持 Redis首选和内存缓存降级两种存储后端。
"""
from datetime import timedelta
from typing import ClassVar
from cachetools import TTLCache
from loguru import logger as l
from . import RedisManager
class TokenStore:
"""
一次性令牌存储管理器
根据 Redis 可用性自动选择存储后端:
- Redis 可用:使用 Redis支持分布式部署
- Redis 不可用:使用内存缓存(仅单实例)
"""
_memory_cache: ClassVar[TTLCache[str, bool]] = TTLCache(maxsize=10000, ttl=3600)
"""内存缓存降级方案"""
@classmethod
async def mark_used(cls, jti: str, ttl: timedelta | int) -> bool:
"""
标记令牌为已使用(原子操作)。
:param jti: 令牌唯一标识符JWT ID
:param ttl: 过期时间
:return: True 表示首次标记成功可以使用False 表示已被使用
"""
ttl_seconds = int(ttl.total_seconds()) if isinstance(ttl, timedelta) else ttl
client = RedisManager.get_client()
if client is not None:
# 使用 Redis SETNX 原子操作
key = f"download_token:{jti}"
result = await client.set(key, "1", nx=True, ex=ttl_seconds)
return result is not None
else:
# 降级使用内存缓存
if jti in cls._memory_cache:
return False
cls._memory_cache[jti] = True
return True
@classmethod
async def is_used(cls, jti: str) -> bool:
"""
检查令牌是否已被使用。
:param jti: 令牌唯一标识符
:return: True 表示已被使用
"""
client = RedisManager.get_client()
if client is not None:
key = f"download_token:{jti}"
return await client.exists(key) > 0
else:
return jti in cls._memory_cache