Init
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user