feat: 强化打包配置与服务安全
This commit is contained in:
+47
-9
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user