feat: 强化打包配置与服务安全

This commit is contained in:
shen
2026-07-18 13:30:56 +08:00
parent d1f070b251
commit a48909f3bc
26 changed files with 779 additions and 203 deletions
+47 -9
View File
@@ -1,16 +1,24 @@
"""认证 API"""
import jwt
import threading
import time
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from ..schemas import LoginRequest, LoginResponse
from ..config import JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_HOURS
from ..config import (
JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_HOURS,
LOGIN_RATE_LIMIT_MAX_ATTEMPTS, LOGIN_RATE_LIMIT_WINDOW_SECONDS, LOGIN_RATE_LIMIT_LOCKOUT_SECONDS,
)
from ..deps import get_current_user
from ..database import get_db
from ..models import User
from ..security import hash_password, verify_password
router = APIRouter(prefix="/api/auth", tags=["auth"])
_login_attempts: dict[str, tuple[int, float, float]] = {}
_login_attempts_lock = threading.Lock()
def _create_token(username: str, is_admin: bool) -> str:
@@ -23,19 +31,49 @@ def _create_token(username: str, is_admin: bool) -> str:
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
def _verify_password(password: str, password_hash: str) -> bool:
"""校验密码"""
import hashlib
return hashlib.sha256(password.encode()).hexdigest() == password_hash
def _check_login_rate_limit(client_ip: str) -> None:
"""按来源 IP 限制登录失败次数,降低在线暴力破解风险。"""
now = time.monotonic()
with _login_attempts_lock:
attempts, first_failure, locked_until = _login_attempts.get(client_ip, (0, now, 0))
if locked_until > now:
raise HTTPException(status_code=429, detail="登录失败次数过多,请稍后再试")
if now - first_failure > LOGIN_RATE_LIMIT_WINDOW_SECONDS:
_login_attempts.pop(client_ip, None)
def _record_login_failure(client_ip: str) -> None:
now = time.monotonic()
with _login_attempts_lock:
attempts, first_failure, _ = _login_attempts.get(client_ip, (0, now, 0))
if now - first_failure > LOGIN_RATE_LIMIT_WINDOW_SECONDS:
attempts, first_failure = 0, now
attempts += 1
locked_until = now + LOGIN_RATE_LIMIT_LOCKOUT_SECONDS if attempts >= LOGIN_RATE_LIMIT_MAX_ATTEMPTS else 0
_login_attempts[client_ip] = (attempts, first_failure, locked_until)
def _clear_login_failures(client_ip: str) -> None:
with _login_attempts_lock:
_login_attempts.pop(client_ip, None)
@router.post("/login", response_model=LoginResponse)
async def login(request: LoginRequest, db: Session = Depends(get_db)):
async def login(credentials: LoginRequest, request: Request, db: Session = Depends(get_db)):
"""用户登录"""
user = db.query(User).filter(User.username == request.username).first()
if not user or not _verify_password(request.password, user.password_hash):
client_ip = request.client.host if request.client else "unknown"
_check_login_rate_limit(client_ip)
user = db.query(User).filter(User.username == credentials.username).first()
valid, needs_upgrade = verify_password(credentials.password, user.password_hash) if user else (False, False)
if not user or not valid:
_record_login_failure(client_ip)
raise HTTPException(status_code=401, detail="用户名或密码错误")
if needs_upgrade:
user.password_hash = hash_password(credentials.password)
db.commit()
_clear_login_failures(client_ip)
token = _create_token(user.username, user.is_admin)
return LoginResponse(token=token, username=user.username, is_admin=user.is_admin)