"""Session 核心:登录会话的签发与校验 存储后端为 Redis(key: session:{token},JSON 序列化,TTL 12h); 构造时传入 None 降级为进程级内存 dict(带过期时间戳,惰性清理过期项,重启失效)。 所有 Redis 读写异常统一包装为 SessionStoreError 抛出(不静默)。 """ import json import secrets import time from redis import asyncio as redis_async # Redis 键前缀 _SESSION_KEY_PREFIX = "session:" class SessionStoreError(Exception): """Session 存储后端读写异常""" class SessionStore: """会话存储:Redis 持久化(带 TTL),构造传 None 降级为内存 dict""" TTL: int = 43200 # 会话有效期(秒),12h def __init__(self, redis_client: redis_async.Redis | None) -> None: # redis.asyncio 客户端(需 decode_responses=True);None → 内存降级 self._redis = redis_client # 内存降级:token -> (payload, 过期时间戳) self._memory: dict[str, tuple[dict[str, str], float]] = {} @staticmethod def _key(token: str) -> str: return f"{_SESSION_KEY_PREFIX}{token}" async def create(self, username: str, role: str) -> str: """签发会话:生成 64 位 hex token,存 session:{token} = {username, role}""" token = secrets.token_hex(32) payload = {"username": username, "role": role} if self._redis is None: self._purge_expired() self._memory[token] = (payload, time.time() + self.TTL) return token try: await self._redis.setex(self._key(token), self.TTL, json.dumps(payload, ensure_ascii=False)) except Exception as e: raise SessionStoreError("写入会话失败") from e return token async def get(self, token: str) -> dict[str, str] | None: """校验会话:无效或已过期返回 None,有效返回 {username, role}""" if self._redis is None: item = self._memory.get(token) if item is None: return None payload, expire_at = item if expire_at <= time.time(): # 惰性清理过期项 del self._memory[token] return None return dict(payload) try: raw = await self._redis.get(self._key(token)) except Exception as e: raise SessionStoreError("读取会话失败") from e if raw is None: return None return json.loads(raw) async def delete(self, token: str) -> None: """删除单个会话(幂等)""" if self._redis is None: self._memory.pop(token, None) return try: await self._redis.delete(self._key(token)) except Exception as e: raise SessionStoreError("删除会话失败") from e async def delete_by_username(self, username: str) -> None: """删除指定用户的全部会话(删除用户时清理其登录态)""" if self._redis is None: matched = [t for t, (payload, _) in self._memory.items() if payload["username"] == username] for token in matched: del self._memory[token] return try: async for key in self._redis.scan_iter(match=f"{_SESSION_KEY_PREFIX}*"): raw = await self._redis.get(key) if raw is not None and json.loads(raw).get("username") == username: await self._redis.delete(key) except Exception as e: raise SessionStoreError(f"按用户删除会话失败: {username}") from e def _purge_expired(self) -> None: """惰性清理内存降级模式下的过期会话""" now = time.time() for token in [t for t, (_, expire_at) in self._memory.items() if expire_at <= now]: del self._memory[token]