"""FastAPI 依赖注入:DB / Redis / 当前身份(用户或游客)/ 管理员""" from dataclasses import dataclass import redis.asyncio as aioredis from fastapi import Depends, Header from sqlalchemy.ext.asyncio import AsyncSession from app.core import errors from app.core.response import BizException from app.core.security import decode_token from app.database import get_db from app.redis_client import get_redis TOKEN_TYPE_ACCESS = "access" @dataclass class Identity: """请求身份:登录用户(user_id 非空)或游客(device_id 非空)""" user_id: int | None device_id: str | None @property def is_user(self) -> bool: return self.user_id is not None @property def is_guest(self) -> bool: return self.user_id is None and self.device_id is not None @property def subject(self) -> str: """计费主体标识:注册用户=user_id,游客=device_id""" return str(self.user_id) if self.user_id is not None else (self.device_id or "") async def get_identity( authorization: str | None = Header(default=None), x_device_id: str | None = Header(default=None), redis: aioredis.Redis = Depends(get_redis), ) -> Identity: """解析顺序:Bearer Token 有效 → 用户态;否则取 X-Device-ID → 游客态""" if authorization and authorization.startswith("Bearer "): token = authorization.removeprefix("Bearer ").strip() try: payload = decode_token(token) except BizException: payload = None if payload and payload.get("type") == TOKEN_TYPE_ACCESS: # 登出/改密的 jti 黑名单校验 jti = payload.get("jti", "") if jti and await redis.exists(f"token:blacklist:{jti}"): payload = None else: # 令牌纪元校验:改密/注销后旧 Token 全端失效 user_id = int(payload["sub"]) epoch = int(await redis.get(f"auth:epoch:{user_id}") or 0) if int(payload.get("ep", 0)) != epoch: payload = None if payload: return Identity(user_id=int(payload["sub"]), device_id=x_device_id) return Identity(user_id=None, device_id=x_device_id) async def require_user(identity: Identity = Depends(get_identity)) -> Identity: """仅登录用户可用""" if identity.user_id is None: raise BizException(errors.UNAUTHORIZED, http_status=401) return identity @dataclass class AdminIdentity: admin_id: int role: str # super / reviewer / operator async def get_admin( authorization: str | None = Header(default=None), redis: aioredis.Redis = Depends(get_redis), ) -> AdminIdentity: if not authorization or not authorization.startswith("Bearer "): raise BizException(errors.UNAUTHORIZED, http_status=401) payload = decode_token(authorization.removeprefix("Bearer ").strip()) token_type = payload.get("type", "") if not token_type.startswith("admin:"): raise BizException(errors.ADMIN_FORBIDDEN, http_status=403) jti = payload.get("jti", "") if jti and await redis.exists(f"token:blacklist:{jti}"): raise BizException(errors.UNAUTHORIZED, http_status=401) return AdminIdentity(admin_id=int(payload["sub"]), role=token_type.split(":", 1)[1]) def require_roles(*roles: str): """后台角色鉴权:super 拥有全部权限""" async def checker(admin: AdminIdentity = Depends(get_admin)) -> AdminIdentity: if admin.role != "super" and admin.role not in roles: raise BizException(errors.ADMIN_FORBIDDEN, http_status=403) return admin return checker