Регистрация пользователей и шифрование паролей

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

У нас с вами были сформированы функции представления для главной страницы, а также для добавления статьи и ее отображения. Во всех этих функциях имеется общий код соединения с БД, который следует вынести как бы за скобки и описать в функции декоратора before_request:

dbase = None
@app.before_request
def before_request():
    """Установление соединения с БД перед выполнением запроса"""
   global dbase
    db = get_db()
    dbase = FDataBase(db)

А в функциях обработчиках запросов это соединение просто использовать.

Подготовка форм авторизации и регистрации

Теперь предположим, что на нашем сайте мы хотели бы выполнять регистрацию и авторизацию пользователей. С чего здесь следует начать? Для начала добавим пункт в главное меню «Авторизация», т.е. в БД таблицы mainmenu следует создать запись:

Авторизация, /login

И, затем, добавим функцию представления для URL /login:

@app.route("/login")
def login():
    return render_template("login.html", menu=dbase.getMenu(), title="Авторизация")

Создадим шаблон login.html:

{% extends 'base.html' %}
 
{% block content %}
{{ super() }}
{% for cat, msg in get_flashed_messages(True) %}
<div class="flash {{cat}}">{{msg}}</div>
{% endfor %} 
<form action="/login" method="post" class="form-contact">
<p><label>Email: </label> <input type="text" name="email" value="" requied />
<p><label>Пароль: </label> <input type="password" name="psw" value="" requied />
<p><input type="checkbox" name="remainme" /> Запомнить меня
<p><input type="submit" value="Войти" />
<hr align=left width="300px">
<p><a href="{{url_for('register')}}">Регистрация</a>
</form>
{% endblock %}

Здесь у нас простая форма авторизации по Email и паролю, а также имеется ссылка на страницу регистрации, если пользователь еще не зарегистрирован.

Соответственно, в программе нужно добавить обработчик для регистрации:

@app.route("/register")
def register():
    return render_template("register.html", menu=dbase.getMenu(), title="Регистрация")

И прописать шаблон register.html:

{% extends 'base.html' %}
 
{% block content %}
{{ super() }}
{% for cat, msg in get_flashed_messages(True) %}
<div class="flash {{cat}}">{{msg}}</div>
{% endfor %}
<form action="/register" method="post" class="form-contact">
<p><label>Имя: </label> <input type="text" name="name" value="" requied />
<p><label>Email: </label> <input type="text" name="email" value="" requied />
<p><label>Пароль: </label> <input type="password" name="psw" value="" requied />
<p><label>Повтор пароля: </label> <input type="password" name="psw2" value="" requied />
<p><input type="submit" value="Регистрация" />
</form>
{% endblock %}

Здесь все очень похоже на форму авторизации, только прописаны другие поля и обработка данных по запросу /register.

После всего этого подготовительного этапа, пришло время создать таблицу в БД, которая бы хранила данные о зарегистрированных пользователях. Мы ее сделаем простой: назовем users и определим следующие поля:

CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
name text NOT NULL,
email text NOT NULL,
psw text NOT NULL,
time integer NOT NULL
);

Создадим ее в БД, выполним функцию create_db в консоли Python:

from flsite import create_db
create_db()

Если кто не помнит, мы эту функцию создавали ранее, когда говорили об использовании БД во Flask. Я не буду здесь останавливаться на этом моменте, если есть необходимость, просто посмотрите это видео.

Шифрование паролей в БД

Итак, мы создали таблицу users в БД flsite и пришла пора ее наполнить, т.е. написать функционал, который бы добавлял нового пользователя в эту таблицу. Как это правильно сделать? Если мы будем создавать записи один в один с введенными значениями в форме регистрации, то в поле psw пароли будут храниться в открытом (не защищенном) виде:

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

PBKDF2 (Password-Based Key Derivation Function)

который доступен из пакета

Werkzeug

устанавливаемый совместно с Flask.

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

pbkdf2:sha256:150000$Xi7Spouc$60425441c622d897c2ae4dce97ee1a5a3f522c0bbb4f72c87c15f3d0fcc227b1

В шифровании это называется хешем закодированных данных. И глядя на этот хеш, совершенно непонятно что в нем записано. Причем, нет обратного алгоритма декодирования, который бы из этой строки получал пароль 12345. Поэтому, у злоумышленников здесь только один вариант – полный перебор. То есть, перебирать все возможные варианты паролей, кодировать их и сравнивать полученный хеш с хешем в БД. Но это очень долгий и сложный путь. Поэтому так важно им знать приватную информацию в чистом виде. И мы им такой радости не доставим, все нужное будем кодировать! А как мы тогда на сервере проверим, например, корректность ввода пароля? Для этого, пароль, указанный пользователем подвергается шифрованию и, затем, сравниваются между собой их хеши. Если они равны, значит, пароль верный.

Итак, как во Flask выполнять кодирование и последующую проверку пароля. Для этого из пакета Werkzeug нужно импортировать следующие функции:

from werkzeug.security import generate_password_hash, check_password_hash
  • generate_password_hash() – выполняет кодирование строки данных по стандарту PBKDF2;
  • check_password_hash() – выполняет проверку указанных данных на соответствие хеша.

Например, чтобы закодировать пароль, достаточно выполнить:

hash = generate_password_hash("12345")

а для проверки:

check_password_hash(hash, "12345")

Последняя функция вернет True, если хеши совпадают и False в противном случае.

Добавление пользователя в БД

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

from werkzeug.security import generate_password_hash, check_password_hash

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

@app.route("/register", methods=["POST", "GET"])
def register():
    if request.method == "POST":
        session.pop('_flashes', None)
        if len(request.form['name']) > 4 and len(request.form['email']) > 4 \
            and len(request.form['psw']) > 4 and request.form['psw'] == request.form['psw2']:
            hash = generate_password_hash(request.form['psw'])
            res = dbase.addUser(request.form['name'], request.form['email'], hash)
            if res:
                flash("Вы успешно зарегистрированы", "success")
                return redirect(url_for('login'))
            else:
                flash("Ошибка при добавлении в БД", "error")
        else:
            flash("Неверно заполнены поля", "error")
 
    return render_template("register.html", menu=dbase.getMenu(), title="Регистрация")

Здесь все достаточно очевидно и нам нужно еще прописать метод addUser во вспомогательном классе FDataBase:

    def addUser(self, name, email, hpsw):
        try:
            self.__cur.execute(f"SELECT COUNT() as `count` FROM users WHERE email LIKE '{email}'")
            res = self.__cur.fetchone()
            if res['count'] > 0:
                print("Пользователь с таким email уже существует")
                return False
 
            tm = math.floor(time.time())
            self.__cur.execute("INSERT INTO users VALUES(NULL, ?, ?, ?, ?)", (name, email, hpsw, tm))
            self.__db.commit()
        except sqlite3.Error as e:
            print("Ошибка добавления пользователя в БД "+str(e))
            return False
 
        return True

Сначала идет проверка на уникальность email для пользователя и если указанного email не существует, то пользователь добавляется в БД.

Видео по теме