Авторизация пользователей на сайте через Flask-Login

Файл проекта: https://github.com/selfedu-rus/flasksite-16

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

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

Flask-Login

который значительно упрощает эту задачу. Им мы и воспользуемся. Сначала его нужно установить. Это делается с помощью команды:

pip install flask-login

И, далее, импортировать его, а точнее, класс LoginManager, который управляет процессом авторизации:

from flask_login import LoginManager

Ну и, затем, создать экземпляр этого класса, например, так:

login_manager = LoginManager(app)

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

Далее, в обработчике авторизации пользователя создается объект класса UserLogin и передается специальной функции login_user, которая определена в модуле Flask-Login:

Данная функция заносит в сессию информацию о зарегистрированном пользователе, используя определенные в классе методы. После этого сессионная информация будет присутствовать во всех запросах к серверу. Для ее обработки во Flask-Login определен специальный декоратор:

@login_manager.user_loader
def load_user(user_id):
    print("load_user")
    return UserLogin().fromDB(user_id, dbase)

Причем функции load_user этого декоратора будет передан идентификатор пользователя, который присутствует в сессии запроса. Фактически, это будет user_id, который возвращается методом get_id класса UserLogin.

Зная этот идентификатор, мы можем обратиться к БД, прочитать данные этого пользователя и создать экземпляр класса UserLogin на основе прочитанных данных. Благодаря этому, у нас при каждом запросе в системе гарантированно будет объект класса UserLogin, с которым мы сможем в дальнейшем спокойно работать.

Также следует иметь в виду, что функция декоратора user_loader вызывается после функции декоратора

before_query

это гарантирует нам наличие подключения к БД в функции load_user, в которой используется переменная dbase.

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

@app.route("/post/<alias>")
@login_required
def showPost(alias):
…

И теперь страница доступна только авторизованным посетителям сайта.

Начальная реализация авторизации

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

class UserLogin:
    def fromDB(self, user_id, db):
        self.__user = db.getUser(user_id)
        return self
 
    def create(self, user):
        self.__user = user
        return self
 
    def is_authenticated(self):
        return True
 
    def is_active(self):
        return True
 
    def is_anonymous(self):
        return False
 
    def get_id(self):
        return str(self.__user['id'])

Здесь первый метод fromDB используется при создании объекта в декораторе user_loader. Он по user_id выполняет загрузку пользовательских данных из БД и сохраняет в частном свойстве __user. Второй метод create используется при создании объекта в момент авторизации пользователя. Вся информация о нем уже известна и мы ее просто передаем по ссылке user и также сохраняем в частной переменной __user. Эта информация потом пригодится в методе get_id, который возвращает id текущего пользователя.

Далее, нам нужно сразу же определить метод getUser в классе FDataBase. Он будет следующий:

    def getUser(self, user_id):
        try:
            self.__cur.execute(f"SELECT * FROM users WHERE id = {user_id} LIMIT 1")
            res = self.__cur.fetchone()
            if not res:
                print("Пользователь не найден")
                return False 
 
            return res
        except sqlite3.Error as e:
            print("Ошибка получения данных из БД "+str(e))
 
        return False

Все предельно просто. Мы извлекаем из таблицы users все поля для указанного id пользователя и, затем, возвращаем прочитанные данные. Если данные прочитать не удалось, то метод возвращает значение False.

После объявления класса UserLogin, мы можем его использовать в нашей основной программе. Сначала сделаем его импорт:

from UserLogin import UserLogin

а, затем, пропишем декоратор

@login_manager.user_loader
def load_user(user_id):
    print("load_user")
    return UserLogin().fromDB(user_id, dbase)

Как мы отмечали, здесь будет создаваться объект UserLogin при каждом запросе, если пользователь авторизован. Если не авторизован, то функция вызываться не будет.

Ну и, наконец, самое главное – прописать обработчик для авторизации пользователя. Я его приведу в таком упрощенном виде:

@app.route("/login", methods=["POST", "GET"])
def login():
    if request.method == "POST":
        user = dbase.getUserByEmail(request.form['email'])
        if user and check_password_hash(user['psw'], request.form['psw']):
            userlogin = UserLogin().create(user)
            login_user(userlogin)
            return redirect(url_for('index'))
 
        flash("Неверная пара логин/пароль", "error")
 
    return render_template("login.html", menu=dbase.getMenu(), title="Авторизация")

Здесь сначала идет проверка: если данные были переданы из формы авторизации, то мы обращаемся к БД и считываем информацию о пользователе, опираясь на указанный в форме email. Если данные из БД были прочитаны успешно и верно введен пароль, то формируется объект класса UserLogin и вызывается функция login_user. С этого момента пользователь считается авторизованным. Далее идет перенаправление на главную страницу сайта. Если какие-либо проверки не прошли, то формируется мгновенное сообщение и снова отображается форма авторизации.

И последнее, что мы сделаем перед пробным запуском, это ограничим доступ к страницам статей неавторизованным посетителям:

@app.route("/post/<alias>")
@login_required
def showPost(alias):
    title, post = dbase.getPost(alias)
    if not title:
        abort(404)
 
    return render_template('post.html', menu=dbase.getMenu(), title=title, post=post)

Все, теперь при запуске мы можем авторизовываться и просматривать статьи только в этом состоянии.

Видео по теме