fix(security): Tailscale内网(100.64/10)免API_KEY+外部强制校验;修复账号重复提交400(提交锁+唯一约束兜底)

This commit is contained in:
gouki
2026-08-15 22:19:51 +00:00
parent b4705331c0
commit 8fea6c2288
6 changed files with 88 additions and 16 deletions
+46 -5
View File
@@ -1,17 +1,26 @@
"""API Key 认证依赖
用于保护写操作(POST/PUT/DELETE)。
- 若 settings.API_KEY 为空,则放行所有请求(纯内网场景)。
- 若已配置,则要求请求头携带正确的 X-API-Key,否则返回 401
- 来自可信内网(Tailscale 100.64.0.0/10、本机回环)的请求直接放行;
- 其余来源:API_KEY 已配置则校验 X-API-Key,未配置则拒绝(防止外部裸奔)
"""
import hmac
import ipaddress
from typing import Optional
from fastapi import Header, HTTPException, status
from fastapi import Header, HTTPException, Request, status
from app.core.config import settings
# 可信来源网段:Tailscale 使用 CGNAT 100.64.0.0/10 分配内网 IP(如 100.89.x.x);
# 回环地址覆盖 tailscale serve 代理转发与本地开发场景。
TRUSTED_NETWORKS = [
ipaddress.ip_network("100.64.0.0/10"),
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("::1/128"),
]
def _key_matches(provided: Optional[str], expected: str) -> bool:
"""常量时间比较密钥,避免时序旁路泄露密钥长度/前缀信息"""
@@ -20,12 +29,44 @@ def _key_matches(provided: Optional[str], expected: str) -> bool:
return hmac.compare_digest(provided.encode(), expected.encode())
def _client_ip(request: Request) -> str:
"""获取客户端真实 IP
tailscale serve 转发到本机时 request.client 为回环地址,真实来源在
X-Forwarded-For 头中;仅信任来自回环的 XFF,防止外部直连时伪造
X-Forwarded-For 绕过鉴权。
"""
host = request.client.host if request.client else ""
if host in ("127.0.0.1", "::1"):
xff = request.headers.get("x-forwarded-for")
if xff:
return xff.split(",")[0].strip()
return host
def _is_trusted(ip: str) -> bool:
"""判断来源 IP 是否属于可信内网(Tailscale 网段 / 本机回环)"""
if not ip:
return False
try:
addr = ipaddress.ip_address(ip)
except ValueError:
return False
return any(addr in net for net in TRUSTED_NETWORKS)
async def require_api_key(
request: Request,
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
) -> None:
"""校验 API Key(可选启用)"""
if not settings.API_KEY:
"""校验 API KeyTailscale 内网/本机放行,外部来源必须携带有效 Key"""
if _is_trusted(_client_ip(request)):
return
if not settings.API_KEY:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="外部访问需要 API Key,请先在 .env 配置 API_KEY 并设置到前端",
)
if not _key_matches(x_api_key, settings.API_KEY):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,