6c6f690788
- 移除冗余依赖包 - 新增账号禁用校验与用户管理能力 - 新增文档下载与管理页面文件展示 - 新增API文档页面与用户管理前端页面 - 重构时区处理与docker-compose部署配置 - 完善测试用例与项目文档
99 lines
3.7 KiB
Python
99 lines
3.7 KiB
Python
"""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
|