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

Хранение и проверка паролей

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

В security-lab нет публичной регистрации или восстановления через email. Два пользователя создаются локальной командой подготовки; пароли выбирает читатель для своего стенда. Результат урока — понятный договор хранения и проверки, который следующий урок соединит с сессией.

Почему обычный хеш недостаточен

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

Соль различает результаты для одинаковых паролей. Она не является секретным ключом, который нужно прятать отдельно от хеша. Важно, чтобы библиотека формировала её правильно и хранила необходимые параметры вместе с результатом.

В примере используем generate_password_hash и check_password_hash из Werkzeug. Выбираем метод scrypt; конкретное строковое представление хранит сведения, необходимые проверке. API описан в документации Werkzeug. Самостоятельный алгоритм шифрования паролей здесь не нужен.

Запись пользователя

Пользователь получает числовой ID, учебное имя и password_hash. Поле auth_version пригодится для отзыва прежних сессий. Это серверное состояние, которое не выбирается браузером.

CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    auth_version INTEGER NOT NULL DEFAULT 1,
    active INTEGER NOT NULL DEFAULT 1
);

В наших данных Alice имеет ID 1, Boris — ID 2. Заметки ссылаются на эти числа. Изменение отображаемого названия пользователя не должно менять владельца всех объектов. Такое разделение личности и подписи полезно и для обычной организации базы.

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

Подготовка двух владельцев

Ниже содержательный фрагмент локальной команды подготовки. Соединение db и схема уже созданы; функция не является HTTP-маршрутом.

from getpass import getpass
from werkzeug.security import generate_password_hash

for user_id, name in ((1, "alice"), (2, "boris")):
    password = getpass("Учебный пароль для " + name + ": ")
    if not 12 <= len(password) <= 128:
        raise ValueError("Выберите пароль длиной 12–128 символов")
    db.execute(
        "INSERT INTO users(id, name, password_hash) VALUES (?, ?, ?)",
        (user_id, name, generate_password_hash(password, method="scrypt")),
    )
db.commit()

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

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

Команда должна завершаться отказом при уже подготовленной базе вместо перезаписи владельцев. Это сохраняет соответствие заметок и ID. В полном исходнике финального урока такая проверка выполняется до ввода паролей и добавления данных.

Проверка введённого значения

Сначала найдём пользователя по разрешённому имени. Затем передадим хеш и пароль библиотеке. Результат проверки представляет только совпадение с учётными данными; сессия ещё не создана.

from werkzeug.security import check_password_hash

def verify_credentials(db, name, password):
    user = db.execute(
        "SELECT * FROM users WHERE name = ? AND active = 1",
        (name,),
    ).fetchone()
    if user is None:
        return None
    if not check_password_hash(user["password_hash"], password):
        return None
    return user

Имя в нашей модели ограничено коротким латинским идентификатором. Пароль сохраняется в том виде, как введён. Не применяйте к нему lower, trim или нормализацию «для удобства» после создания учётной записи: это меняет проверяемый секрет.

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

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

Для выбранного пароля проверка должна вернуть пользователя, для другого — None. Два вызова generate_password_hash одного пароля обычно создают разные строки из-за соли; обе должны успешно проверяться с исходным значением. Сравнение строк хешей между собой не заменяет check_password_hash.

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

Не возвращайте посетителю сообщение «имя существует, пароль неверен». В форме используется одна общая фраза для ошибки входа. Однако скрытие имени не мешает подбору само по себе; для этого потребуется политика попыток.

Изменение пароля и дальнейшие границы

При смене пароля создаётся новый хеш. Старый исходный секрет не извлекается из базы и не отправляется пользователю. После такого изменения может понадобиться отзыв прежних сессий; в нашей модели это связано с auth_version.

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

Ошибка и ресурс проверки

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

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

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

Для чтения хеша из дампа не требуется работающий сервер. Поэтому лимит HTTP-попыток не защищает от перебора после утечки базы. Он ограничивает другой путь — онлайн-обращения. Сопровождение должно понимать оба сценария и не считать хороший limiter заменой защите копий.

Смена библиотеки может изменить параметры создания новых хешей. Сохраняйте строковый формат и проверку старых записей по документированному API, а не сравнивайте длину хеша с одной константой.

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