up
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user