feat: 核心模块增强 — 加密 + 运行时配置 + 日志缓冲

- crypto.py: API Key 加密/解密工具
- runtime_config.py: 运行时动态配置管理
- log_buffer.py: 内存日志缓冲区
- config.py: 新增加密配置项
- http_client.py: 增强重试和错误处理
This commit is contained in:
shangfangjian
2026-09-19 11:58:03 +08:00
parent b3e2c52b49
commit 786f10aa11
57 changed files with 3178 additions and 488 deletions
+77 -16
View File
@@ -1,37 +1,98 @@
"""API 依赖:鉴权等横切关注点。
审查报告 P2-7:ingest / backtest / settle 这类「写入型或高成本」接口此前
完全无鉴权 —— 任何能访问到服务的人都可触发采集、或直接烧掉 LLM 额度。
策略(渐进式,不破坏本地开发):
- `ADMIN_API_KEY` 未配置 → 直接放行,并打一次 warning
这样本地 `docker compose up` 无需额外配置即可用。
- 已配置 → 必须带匹配的 `X-API-Key` 请求头,否则 401
- 管理员密码:库中 scrypt 哈希优先,回落 .env 初始值;后台可在线修改
- 密码已配置(哈希或 .env)→ 管理后台可用密码登录,登录后颁发 HttpOnly
Cookie 会话;受保护接口接受 Cookie 会话或 X-API-Key
- 仅配置 `ADMIN_API_KEY` → 受保护接口只接受 `X-API-Key` 请求头(机器/脚本调用)。
- 两者都未配置 → 直接放行,并打一次 warning(本地开发模式)。
"""
from __future__ import annotations
import hmac
import logging
import secrets
import time
from fastapi import Header, HTTPException
from fastapi import Header, HTTPException, Request
from src.core.config import settings
from src.core.runtime_config import get_admin_password_hash
logger = logging.getLogger(__name__)
SESSION_COOKIE = "profeto_session"
async def require_admin_key(x_api_key: str | None = Header(None, alias="X-API-Key")) -> None:
"""保护「写入型 / 高成本」接口的依赖。
用法: `@router.post("/ingest/bzzoiro", dependencies=[Depends(require_admin_key)])`
def _sign(exp_ts: int, secret: bytes) -> str:
msg = f"profeto-admin:{exp_ts}".encode()
return hmac.new(secret, msg, "sha256").hexdigest()
async def get_session_secret() -> bytes:
"""会话签名密钥 = HMAC(SECRET_KEY, 管理员凭证指纹)。
指纹来自密码哈希(密码本身永不参与签名):密码变更 → 指纹变化
→ 全部旧会话失效,无需额外吊销机制。
"""
expected = settings.ADMIN_API_KEY
if not expected:
from src.core.runtime_config import get_admin_credential_fingerprint
fingerprint = await get_admin_credential_fingerprint()
key = settings.SECRET_KEY or f"fallback:{settings.DATABASE_URL}"
return hmac.new(key.encode(), b"session:" + fingerprint.encode(), "sha256").digest()
def create_session_token(secret: bytes) -> str:
exp_ts = int(time.time()) + settings.ADMIN_SESSION_TTL_HOURS * 3600
return f"{exp_ts}.{_sign(exp_ts, secret)}"
def verify_session_token(token: str, secret: bytes) -> bool:
try:
exp_raw, sig = token.split(".", 1)
exp_ts = int(exp_raw)
if exp_ts < int(time.time()):
return False
return secrets.compare_digest(sig, _sign(exp_ts, secret))
except (ValueError, TypeError):
return False
async def auth_configured() -> bool:
"""是否已启用鉴权(密码哈希/.env 密码/API Key 任一)。"""
return bool(
await get_admin_password_hash()
or settings.ADMIN_PASSWORD
or settings.ADMIN_API_KEY
)
async def require_admin(
request: Request,
x_api_key: str | None = Header(None, alias="X-API-Key"),
) -> None:
"""统一保护管理接口:接受 Cookie 会话(密码登录)或 X-API-Key。
用法: `@router.get("/leagues", dependencies=[Depends(require_admin)])`
"""
if not await auth_configured():
logger.warning(
"ADMIN_API_KEY 未设置,采集/回测接口当前【无鉴权】。"
"生产环境请设置该环境变量"
"管理员密码 / ADMIN_API_KEY 未设置,管理接口当前【无鉴权】。"
"生产环境请至少设置其中一项"
)
return
if not x_api_key or not secrets.compare_digest(x_api_key, expected):
raise HTTPException(status_code=401, detail="无效或缺失的 X-API-Key")
# 1) Cookie 会话(密码登录颁发)
token = request.cookies.get(SESSION_COOKIE)
if token and verify_session_token(token, await get_session_secret()):
return
# 2) X-API-Key(机器/脚本调用;key 明文只在内存中,与第三方交互必需)
if (
settings.ADMIN_API_KEY
and x_api_key
and secrets.compare_digest(x_api_key, settings.ADMIN_API_KEY)
):
return
raise HTTPException(status_code=401, detail="未登录或凭证无效")