feat: 核心模块增强 — 加密 + 运行时配置 + 日志缓冲
- crypto.py: API Key 加密/解密工具 - runtime_config.py: 运行时动态配置管理 - log_buffer.py: 内存日志缓冲区 - config.py: 新增加密配置项 - http_client.py: 增强重试和错误处理
This commit is contained in:
+77
-16
@@ -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="未登录或凭证无效")
|
||||
|
||||
Reference in New Issue
Block a user