This commit is contained in:
2026-06-08 16:09:13 +08:00
commit 518c6b3c7a
32 changed files with 4156 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
"""会话存储:管理 Cloudflare 验证通过后的浏览器会话状态
支持两种存储后端:
- Redis(推荐生产环境使用,支持多实例共享)
- 进程内存(默认回退,仅单实例可用)
会话包含浏览器存储状态(Cookie、localStorage 等)和 cf_clearance 值,
设置 TTL 自动过期,过期后需要重新通过 Cloudflare 挑战。
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass
from datetime import UTC, datetime, timedelta
from typing import Any
from app.core.config import Settings
try:
from redis.asyncio import Redis
except Exception: # pragma: no cover - optional until dependencies are installed.
Redis = Any # type: ignore[misc,assignment]
@dataclass(slots=True)
class SessionState:
"""会话状态数据"""
session_id: str
user_agent: str
proxy_key: str
storage_state: dict[str, Any]
cf_clearance: str | None
created_at: str
expires_at: str
@property
def is_expired(self) -> bool:
"""会话是否已过期"""
return datetime.now(UTC) >= datetime.fromisoformat(self.expires_at)
class SessionStore:
"""会话存储,支持 Redis 和进程内存两种后端"""
def __init__(self, settings: Settings):
self._settings = settings
self._redis: Redis | None = None
self._memory: dict[str, SessionState] = {}
async def start(self) -> None:
"""初始化存储后端:配置了 Redis 时连接 Redis,否则使用进程内存"""
if not self._settings.redis_host:
return
from redis.asyncio import Redis as AsyncRedis
self._redis = AsyncRedis(
host=self._settings.redis_host,
port=self._settings.redis_port,
password=self._settings.redis_password,
db=self._settings.redis_db,
decode_responses=True,
)
await self._redis.ping()
async def close(self) -> None:
"""关闭存储后端连接"""
if self._redis is not None:
await self._redis.aclose()
self._redis = None
@property
def redis_enabled(self) -> bool:
"""Redis 是否已连接"""
return self._redis is not None
def _session_key(self, session_name: str) -> str:
"""生成 Redis/内存中的存储键"""
return f"{self._settings.session_namespace}:session:{session_name}"
async def get_session(self, session_name: str) -> SessionState | None:
"""获取会话状态,过期会话自动失效并返回 None"""
key = self._session_key(session_name)
if self._redis is not None:
raw = await self._redis.get(key)
if not raw:
return None
session = SessionState(**json.loads(raw))
if session.is_expired:
await self.invalidate_session(session_name)
return None
return session
session = self._memory.get(key)
if session and session.is_expired:
self._memory.pop(key, None)
return None
return session
async def save_session(
self,
session_name: str,
session_id: str,
user_agent: str,
proxy_key: str,
storage_state: dict[str, Any],
cf_clearance: str | None,
) -> SessionState:
"""保存会话状态
创建带 TTL 的会话记录,Redis 后端会自动在 TTL 到期后删除。
"""
now = datetime.now(UTC)
state = SessionState(
session_id=session_id,
user_agent=user_agent,
proxy_key=proxy_key,
storage_state=storage_state,
cf_clearance=cf_clearance,
created_at=now.isoformat(),
expires_at=(now + timedelta(seconds=self._settings.session_ttl_seconds)).isoformat(),
)
key = self._session_key(session_name)
if self._redis is not None:
ttl = self._settings.session_ttl_seconds
await self._redis.set(key, json.dumps(asdict(state)), ex=ttl)
return state
self._memory[key] = state
return state
async def invalidate_session(self, session_name: str) -> None:
"""使会话失效,删除存储中的会话记录"""
key = self._session_key(session_name)
if self._redis is not None:
await self._redis.delete(key)
return
self._memory.pop(key, None)