This commit is contained in:
2026-09-09 11:35:02 +08:00
parent bc1c72d558
commit d656c05b3d
35 changed files with 711 additions and 3194 deletions

View File

@@ -1,6 +1,8 @@
"""只登录、不注册的鉴权 API。"""
from __future__ import annotations
import time
from collections import deque
from datetime import timedelta
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response, status
@@ -49,6 +51,33 @@ def clear_session_cookie(response: Response) -> None:
)
# ---------- 登录按 IP 限速(进程内滑动窗口,无外部依赖) ----------
# 补充账户级锁定auth_max_failed_logins本层拦多 IP 分布爆破,也稀释
# 「故意输错 5 次锁死他人账户」的滥用面。反代部署时 host 是代理 IP需改读 X-Forwarded-For。
_LOGIN_WINDOW = 60.0
_LOGIN_MAX_PER_WINDOW = 15
_login_attempts: dict[str, deque[float]] = {}
_login_gc_at = 0.0
def _login_rate_limited(ip: str) -> bool:
"""超限返回 True未超限记录本次尝试成功失败都计"""
global _login_gc_at
now = time.monotonic()
q = _login_attempts.setdefault(ip, deque())
while q and q[0] <= now - _LOGIN_WINDOW:
q.popleft()
if len(q) >= _LOGIN_MAX_PER_WINDOW:
return True
q.append(now)
if now - _login_gc_at > 3600: # 顺手回收陈旧 entry防长跑内存增长
_login_gc_at = now
stale = now - _LOGIN_WINDOW * 10
for k in [k for k, v in _login_attempts.items() if not v or v[-1] <= stale]:
del _login_attempts[k]
return False
@router.post("/login", response_model=LoginResponse)
async def login(
payload: LoginRequest,
@@ -56,6 +85,10 @@ async def login(
response: Response,
db: AsyncSession = Depends(get_session),
) -> LoginResponse:
ip = request.client.host if request.client else "?"
if _login_rate_limited(ip):
raise HTTPException(status_code=429, detail="登录尝试过于频繁,请稍后再试")
now = utcnow()
username = payload.username.strip()
user = (await db.execute(select(User).where(User.username == username))).scalar_one_or_none()