Files
QMDSearch/app/api/deps.py
T
kplam 92b062c048 feat: 新增用户管理(用户增删改查、密码重置、角色权限、会话认证)与 API 指南
- 新增 app/api/deps.py、app/core/users.py、app/core/sessions.py:会话鉴权依赖、
  用户存储(PBKDF2-HMAC-SHA256 + 随机 salt,Redis/内存降级)、会话签发与校验(TTL 12h)
- auth.py 新增用户管理端点(列表/创建/重置密码/删除)与 admin/user 角色权限边界,
  user 访问用户管理返回 1006,禁删自己与最后一个 admin
- admin.html 新增用户管理面板(仅 admin 挂载)与 API 指南在线测试台
- Dockerfile 将 uv 放入 PATH;docker-compose 调整 qdrant 依赖为 service_started
  并移除依赖 curl 的 healthcheck(官方镜像不含 curl)
- 新增用户管理测试(users/sessions/auth_api/auth_integration),全量 461 项测试通过

Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
2026-07-31 21:29:02 +08:00

96 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 并返回当前用户记录
must_change_password 用户被拦截(1006),须先经 POST /auth/password 改密。
"""
user, _ = await _resolve_user(authorization)
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