92b062c048
- 新增 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>
96 lines
3.5 KiB
Python
96 lines
3.5 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 并返回当前用户记录
|
||
|
||
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
|