安全加固 + 数据管线修复 + 质量改进(第2批)

安全加固:
- /api/v1/predict 限流改用 get_client_ip 防 X-Forwarded-For 伪造
- 新增 TRUST_PROXY_HEADERS/REQUIRE_ADMIN_AUTH 配置,默认 fail-closed
- 生产环境管理接口未配置鉴权时拒绝(503),不再放行

数据管线修复:
- injuries IntegrityError 改用 begin_nested(SAVEPOINT)隔离批次
- injuries 空名单 vs 未配置语义区分(has_data 精确标记)
- bzzoiro 统计字段映射(shots/possession/cards) + 入库条件放宽
- available_at 语义收紧(回测防泄漏)
- team_names NFKD 去变音双重查找 + 补齐变体键

预测系统改进:
- multi-agent 全专家失败时 status=degraded 跳过终裁
- agent_weights 独立持久化到 predictions 表

新增迁移:
- 0014_predictions_agent_weights.py

新增测试(8个文件):
- test_ip_spoofing.py: 限流防伪造
- test_require_admin_fail_closed.py: 生产 fail-closed
- test_injuries_integrity_rollback.py: SAVEPOINT 隔离
- test_injuries_slice_semantic.py: 空名单 vs 未配置
- test_available_at.py: 回测防泄漏
- test_bzzoirot_stats.py: 统计字段映射
- test_team_names_normalize.py: NFKD 变体
- test_multi_agent_degraded.py: 全失败 degraded
- test_agent_weights_persist.py: 权重持久化
This commit is contained in:
Profeto Agent
2026-09-19 08:02:57 +00:00
parent bee330f31f
commit 6c24672e87
21 changed files with 1580 additions and 87 deletions
+39 -6
View File
@@ -76,6 +76,17 @@ async def require_admin(
用法: `@router.get("/leagues", dependencies=[Depends(require_admin)])`
"""
if not await auth_configured():
# 生产环境 fail-closed:未配置鉴权则拒绝,不放行
if settings.REQUIRE_ADMIN_AUTH or settings.APP_ENV == "production":
logger.error(
"生产环境管理接口未配置鉴权(REQUIRE_ADMIN_AUTH=True 或 APP_ENV=production),"
"拒绝访问。请设置 ADMIN_PASSWORD 或 ADMIN_API_KEY。"
)
raise HTTPException(
status_code=503,
detail="服务未配置管理鉴权,请联系管理员",
)
# 开发环境 fail-open + warning
logger.warning(
"管理员密码 / ADMIN_API_KEY 均未设置,管理接口当前【无鉴权】。"
"生产环境请至少设置其中一项。"
@@ -98,6 +109,31 @@ async def require_admin(
raise HTTPException(status_code=401, detail="未登录或凭证无效")
# ── 客户端 IP 提取(防 X-Forwarded-For 伪造) ──
def get_client_ip(request: Request) -> str:
"""获取客户端真实 IP,防 X-Forwarded-For 伪造。
规则:
- TRUST_PROXY_HEADERS=False(默认):只用 request.client.host,
忽略 X-Forwarded-For,防止客户端伪造。
- TRUST_PROXY_HEADERS=True:解析 X-Forwarded-For 第一个 IP,
适用于 Nginx 等可信反代后方。
部署建议:
- 公网必须设 TRUST_PROXY_HEADERS=True,并在 Nginx 配置:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- Nginx 层也建议做限流(limit_req),作为第二道防线。
"""
if settings.TRUST_PROXY_HEADERS:
# 信任反代:X-Forwarded-For 可能包含多个 IP(代理链),取第一个
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
# 默认或无反代头:直接用连接层 IP
return request.client.host if request.client else "unknown"
# ── 简易内存限流(按 IP,无外部依赖) ──
class _RateLimiter:
@@ -139,13 +175,10 @@ _predict_limiter = _RateLimiter(max_requests=10, window_seconds=60)
async def rate_limit_predict(request: Request) -> None:
"""POST /api/v1/predict 限流依赖。
基于客户端 IP(考虑 X-Forwarded-For),超过 10 次/分钟返回 429。
基于客户端 IP,超过 10 次/分钟返回 429。
IP 提取逻辑:优先用 get_client_ip(防伪造)。
"""
# 获取客户端 IP(支持反向代理)
client_ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else "unknown")
# X-Forwarded-For 可能包含多个 IP(代理链),取第一个
if "," in client_ip:
client_ip = client_ip.split(",")[0].strip()
client_ip = get_client_ip(request)
if not _predict_limiter.is_allowed(client_ip):
logger.warning("rate limit exceeded for %s", client_ip)