安全加固 + 数据管线修复 + 质量改进(第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:
+39
-6
@@ -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)
|
||||
|
||||
@@ -54,7 +54,9 @@ class PasswordChangeIn(BaseModel):
|
||||
|
||||
|
||||
def _client_ip(request: Request) -> str:
|
||||
return request.client.host if request.client else "unknown"
|
||||
"""获取客户端 IP,与限流共用同一套逻辑(防伪造)。"""
|
||||
from src.api.deps import get_client_ip
|
||||
return get_client_ip(request)
|
||||
|
||||
|
||||
def _is_locked(ip: str) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user