"""API 鉴权依赖:Bearer token → session → 用户记录 UserStore/SessionStore 为模块级懒加载单例:Redis 客户端创建失败时构造传 None, 降级为进程级内存存储(仅开发/兜底,重启失效)。存储后端读写异常统一转 2001。 测试可 monkeypatch _user_store/_session_store 单例完成注入。 """ import structlog from fastapi import Depends, Header from redis import asyncio as redis_async from app.api.response import ApiError from app.config import settings from app.core.sessions import SessionStore, SessionStoreError from app.core.users import UserRecord, UserStore, UserStoreError logger = structlog.get_logger() # 模块级懒加载单例 _user_store: UserStore | None = None _session_store: SessionStore | None = None def _create_redis_client() -> redis_async.Redis | None: """创建 redis.asyncio 客户端(decode_responses=True);失败返回 None 走内存降级""" try: return redis_async.from_url(settings.redis_url, decode_responses=True) except Exception: logger.warning("Redis 客户端创建失败,认证存储降级为内存模式", exc_info=True) return None def _get_user_store() -> UserStore: """用户存储懒加载单例""" global _user_store if _user_store is None: _user_store = UserStore(_create_redis_client()) return _user_store def _get_session_store() -> SessionStore: """会话存储懒加载单例""" global _session_store if _session_store is None: _session_store = SessionStore(_create_redis_client()) return _session_store def _parse_bearer(authorization: str | None) -> str | None: """解析 Authorization 头中的 Bearer token;缺失或格式非法返回 None""" if not authorization: return None scheme, _, token = authorization.partition(" ") if scheme.lower() != "bearer" or not token.strip(): return None return token.strip() async def _resolve_user(authorization: str | None) -> tuple[UserRecord, str]: """Bearer token → session → 用户记录(不拦截 must_change_password) 凭证缺失/无效/用户不存在抛 ApiError(1005);存储后端异常抛 ApiError(2001)。 供 get_current_user 与改密/退出端点共用(后者须对 must_change_password 用户放行)。 """ token = _parse_bearer(authorization) if token is None: raise ApiError(1005, "未认证或凭证无效") try: session = await _get_session_store().get(token) if session is None: raise ApiError(1005, "未认证或凭证无效") user = await _get_user_store().get(session["username"]) except (SessionStoreError, UserStoreError) as exc: raise ApiError(2001, "认证服务暂不可用") from exc if user is None: raise ApiError(1005, "未认证或凭证无效") return user, token async def get_current_user(authorization: str | None = Header(None)) -> UserRecord: """鉴权依赖:校验 Bearer token 并返回当前用户记录 禁用用户被拦截(1005);must_change_password 用户被拦截(1006),须先经 POST /auth/password 改密。 注:logout/password 端点经 _resolve_user 自解析,不在此拦截范围内(改密是已登录用户唯一可用接口)。 """ user, _ = await _resolve_user(authorization) if not user.enabled: raise ApiError(1005, "账号已禁用") if user.must_change_password: raise ApiError(1006, "首次登录须先修改密码") return user async def require_admin(user: UserRecord = Depends(get_current_user)) -> UserRecord: """鉴权依赖:在 get_current_user 之上要求 admin 角色""" if user.role != "admin": raise ApiError(1006, "权限不足") return user