fix(auth): проверка пароля больше не блокирует весь backend
На нагрузочном тесте 31.07.2026 около 70 человек заходили одновременно. Вход развалился: p95 `/api/v1/auth/token` — 7.28 с, p95 `guest-join` — 7.06 с, в БД 33 соединения `idle in transaction` при ОДНОМ активном запросе. Люди попадали внутрь с пятой-десятой попытки, часть не попала вовсе. Медиа при этом работало штатно: 30 участников с 27 камерами в следующем окне прошли без единого лага. Причина — argon2 считался синхронно внутри async-обработчика. Замер на боевом сервере: 95–155 мс на одну проверку, и всё это время event loop процесса стоит целиком. Транзакция БД к тому моменту уже открыта (`get_by_email` сделал SELECT), поэтому соединение висело без работы, пул из 40 выбирался, и отказы получали совершенно посторонние ручки — включая вход в конференцию, где никакого пароля не проверялось. Что изменилось: - `hash_password`/`verify_password` стали асинхронными и считаются в пуле потоков (`asyncio.to_thread`). argon2-cffi освобождает GIL, поэтому проверки идут по-настоящему параллельно; - параметры argon2id заменены с дефолтов библиотеки (t=3, m=64 МБ, p=4) на рекомендацию OWASP (t=2, m=19 МБ, p=1): 95 мс → 42 мс. Отдельно важен `parallelism`: при p=4 одна проверка пароля занимала все четыре ядра сервера — те же, на которых работает LiveKit; - добавлен `needs_rehash`: существующие хэши проверяются как прежде (параметры зашиты в саму строку) и лениво перевыпускаются при первом успешном входе. Расчёт по замерам: пачка из 70 логинов — 6.7–10.9 с блокировки против ~0.36 с без неё. Тесты: event loop продолжает тикать во время проверки; 8 параллельных проверок укладываются заметно быстрее восьми последовательных; хэш со старыми параметрами принимается и перевыпускается при входе.
This commit is contained in:
@@ -24,6 +24,7 @@ from core.security import (
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
hash_password,
|
||||
needs_rehash,
|
||||
verify_password,
|
||||
)
|
||||
from models.email_verification import EmailVerificationToken
|
||||
@@ -131,7 +132,7 @@ class AuthService:
|
||||
user = await self._users.create(
|
||||
email=email,
|
||||
name_user=name_user,
|
||||
password_hash=hash_password(password),
|
||||
password_hash=await hash_password(password),
|
||||
team_id=team_id,
|
||||
)
|
||||
reply_to = cfg.contact_email if cfg.contact_email_enabled else None
|
||||
@@ -161,10 +162,21 @@ class AuthService:
|
||||
async def login(self, *, email: str, password: str) -> TokenPair:
|
||||
"""Проверить учётные данные и выдать пару access/refresh токенов."""
|
||||
user = await self._users.get_by_email(email)
|
||||
if user is None or not verify_password(password, user.password_hash):
|
||||
if user is None or not await verify_password(password, user.password_hash):
|
||||
raise InvalidCredentialsError
|
||||
if not user.email_verified:
|
||||
raise EmailNotVerifiedError
|
||||
|
||||
# Постепенная миграция на актуальные параметры argon2 (см. core/security.py):
|
||||
# параметры зашиты в саму строку хэша, поэтому старые записи так и
|
||||
# проверялись бы вдвое дольше. Открытый пароль есть только здесь и
|
||||
# только сейчас — другого места для перевыпуска не будет.
|
||||
if needs_rehash(user.password_hash):
|
||||
user.password_hash = await hash_password(password)
|
||||
# Явный commit: выдача токенов идёт через Redis и БД не трогает,
|
||||
# поэтому без него перевыпущенный хэш откатился бы вместе с сессией.
|
||||
await self._session.commit()
|
||||
|
||||
return await self._issue_token_pair(user.id, user.role)
|
||||
|
||||
async def refresh(self, refresh_token: str) -> TokenPair:
|
||||
|
||||
Reference in New Issue
Block a user