Перейти к содержанию

Ограничение попыток и нагрузки

Каждый запрос уже имеет проверяемые данные и полномочия. Но корректные по форме обращения могут приходить слишком часто. Проверка scrypt расходует вычислительные ресурсы, а обработка изображения — память и время. Без ограничений одно действие превращается в множество дорогих операций.

Результат урока — небольшой локальный ограничитель попыток и понимание его границ. Он работает в одном процессе учебного стенда. Публичную распределённую защиту нельзя считать выполненной только из-за появления словаря с таймерами.

Что именно ограничивать

Для входа зададим не более пяти попыток за 60 секунд на наблюдаемый адрес и тип операции. Проверка выполняется до дорогого сравнения пароля. В учебном приложении все посещения могут приходить с 127.0.0.1, поэтому этот предел легко увидеть.

Лимит входа не обязан совпадать с пределом чтения заметок. Операции имеют разную стоимость и назначение. Если использовать одну общую очередь на всё, обычное чтение способно мешать входу или наоборот.

Количество сохранённых изображений также является другим правилом. Пять файлов на владельца ограничивает состояние хранилища, а пять попыток в минуту — скорость обращения. Обе границы нужны по своим причинам.

Состояние одного процесса

Ниже полный limiter.py. Он использует монотонное время, lock и ограниченное число ключей. Временные отметки не предназначены для журналирования календарной даты.

from collections import deque
from threading import Lock
import time

class AttemptLimiter:
    def __init__(self, limit=5, window=60, max_keys=1000):
        self.limit = limit
        self.window = window
        self.max_keys = max_keys
        self.items = {}
        self.lock = Lock()

    def allow(self, key):
        now = time.monotonic()
        with self.lock:
            for current, times in list(self.items.items()):
                while times and times[0] <= now - self.window:
                    times.popleft()
                if not times:
                    del self.items[current]
            times = self.items.get(key)
            if times is None:
                if len(self.items) >= self.max_keys:
                    return False
                times = self.items[key] = deque()
            if len(times) >= self.limit:
                return False
            times.append(now)
            return True

Lock связывает проверку количества с добавлением отметки. Без него два параллельных обращения могли бы одновременно увидеть свободное место. Такая синхронизация относится только к текущему процессу, а не к нескольким серверам.

Очистка удаляет устаревшие ключи. Max_keys ограничивает память самого ограничителя. При заполнении новая группа получает отказ, а не бесконечно расширяет словарь. Это простой учебный выбор; реальный продукт должен оценить его влияние на доступность.

Применение перед паролем

В app.py создаём один AttemptLimiter и используем его перед verify_credentials. Ключ включает адрес, который приложение действительно видит, и известное имя операции.

attempts = AttemptLimiter()

key = (request.remote_addr or "unknown", "login")
if not attempts.allow(key):
    abort(429, description="Повторите попытку позже")
user = verify_credentials(db(), name, password)

Этот фрагмент относится к ветке отправки формы входа после проверки её структуры и CSRF. Значение X-Forwarded-For не принимается напрямую от посетителя. Доверие к прокси появится в отдельном уроке.

Одинаковый адрес может принадлежать многим людям за общей сетью. Поэтому лимит IP не является точной моделью личности. В нашем стенде это известное упрощение, которое нужно учитывать при интерпретации отказа.

Подбор и блокировка

Распределённые попытки могут приходить с разных адресов. Один локальный предел не устранит такой сценарий. Публичная система часто сочетает несколько сигналов и ограничения на разных слоях.

При этом безусловная блокировка учётной записи по нескольким чужим ошибкам способна мешать её настоящему владельцу. Политика должна балансировать ограничение перебора и возможность восстановления доступа. Нельзя считать жёсткую вечную блокировку универсальным улучшением.

Подходы к throttling рассматриваются в OWASP Authentication. В этой серии мы используем небольшой механизм только для наблюдения порядка: сначала недорогие проверки и предел, затем дорогая работа.

Ответ 429 не должен раскрывать существование имени. Для неверного пароля применяется общая фраза, а ограничение относится к выбранной группе обращений. Идентификатор запроса пригодится для диагностики отдельной попытки.

Несколько процессов и перезапуск

Если запустить два процесса, каждый получит собственный словарь. Посетитель сможет распределить обращения между ними; общий предел будет отличаться от написанного числа. Перезапуск также очистит состояние.

Поэтому промышленное обслуживание потребует согласованного хранилища или ограничения на доверенном входном слое. У него должны быть атомарные операции, срок записей и известная политика при недоступности. Просто заменить dict на сетевой клиент недостаточно без анализа отказа.

Наша модель не утверждает защиту от DDoS. Приложение уже тратит часть ресурсов до limiter: соединение, разбор HTTP и токен. Ограничения прокси, сервера, сети и задач обработки имеют собственное назначение.

Наблюдение результата

При самостоятельной практике первые пять обращений соответствующей группы в окне допускаются, шестое должно получить отказ до вычисления password_hash. После истечения окна появляется место. Не нужно ждать автоматически при подготовке статьи: наблюдение выполнит читатель на отдельном стенде.

Сохраните условия: адрес, операция, время окна и число процессов. Иначе похожие результаты разных запусков могут иметь разные причины. Если limiter неожиданно сбрасывается, проверьте перезапуск, а не увеличивайте limit наугад.

Попытки загрузки изображения можно ограничить отдельным ключом upload. Но количество самих файлов остаётся условием базы, введённым раньше. Проверка обоих сценариев помогает не спутать скорость и накопленное состояние.

Время и ответ посетителю

Монотонные отметки пригодны для длительности внутри процесса. Их не следует отправлять в отчёт как время события календаря или напрямую переносить между машинами с разным основанием. Общий распределённый механизм потребует собственного согласованного источника времени.

Ответ 429 сообщает, что текущая возможность временно ограничена. Если приложение добавит Retry-After, значение должно соответствовать реально вычисленному окну. Постоянное число 60 после любой попытки может не описывать момент освобождения места.

Клиент не должен бесконечно автоматически повторять дорогую операцию. При отказе полезно сохранить допустимые поля и объяснить возможность следующего действия. Это помогает посетителю, сохраняя ресурсную границу.

Если все пользователи видны с одного прокси, общий ключ IP объединит их попытки. Исправление должно начинаться с известной цепочки доверия и способа идентификации, а не с произвольного роста limit.

Также различайте отказ по квоте хранилища и временный предел. У владельца уже может быть пять сохранённых изображений, поэтому ожидание минуты само по себе не создаст шестое место. Текст интерфейса должен отражать реальную причину, если продукт разрешает их раздельно сообщать.

Теперь приложение ограничивает часть дорогих действий и честно описывает локальную область механизма. Следующий урок рассмотрит сведения, которые остаются в конфигурации, ответе об ошибке и журнале: они также относятся к активам нашей модели.