"""限流配置。 链路是 Caddy → frp → Traefik → 本服务,真实 IP 依赖 uvicorn 的 --proxy-headers(见 entrypoint.sh),否则所有请求会被算作同一来源。 两种维度: - 认证端点按 IP 限流:防止密码爆破(此时还没有用户身份)。 - AI 端点按用户限流:AI 调用花的是用户自己配的 API 额度,且多人可能 共用出口 IP,按 IP 限会互相误伤。 """ from slowapi import Limiter from slowapi.util import get_remote_address from starlette.requests import Request from app.config import settings def _user_or_ip(request: Request) -> str: """优先用 JWT 的 sub 作为限流键,回退到 IP。 这里只做轻量解析,不校验签名——限流键不是安全边界,真正的鉴权 仍由 get_current_user 负责。解析失败就退回 IP。 """ auth = request.headers.get("authorization", "") if auth.lower().startswith("bearer "): token = auth[7:] try: import jwt payload = jwt.decode(token, options={"verify_signature": False}) sub = payload.get("sub") if sub: return f"user:{sub}" except Exception: # noqa: BLE001 - 解析失败就按 IP 限 pass return f"ip:{get_remote_address(request)}" # 默认按 IP 限流(认证端点用:此时还没有用户身份)。 # AI / 上传端点在装饰器上传 key_func=user_key 改为按用户限流。 limiter = Limiter( key_func=get_remote_address, enabled=settings.rate_limit_enabled, # 内存存储:单副本部署够用(SQLite 本来也只能单副本) storage_uri="memory://", # 必须按 endpoint 计数,不能用默认的 "url":像 /attempts/{id}/judge # 这种路径带 ID,按 url 计数会让每次请求各占一个桶,限流形同虚设。 key_style="endpoint", ) # 供 AI / 上传端点使用的按用户限流键 user_key = _user_or_ip # 各类端点的限流策略,集中在这里方便调整 AUTH_LIMIT = settings.rate_limit_auth AI_LIMIT = settings.rate_limit_ai UPLOAD_LIMIT = settings.rate_limit_upload