Аутентификация пользователей в приложении с помощью Flask-Login

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

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

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

Настройка и установка приложения

Исчерпывающее руководство по настройке и установке проекта можно найти в репозитории на GitHub.

Базовая структура приложения

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

Создание приложения

Для начала мы создадим функцию — фабрику приложений внутри файла app.py и назовем ее create_app. Это жизненно важно для любого приложения Flask.

Кроме того, нам нужно импортировать некоторые библиотеки для использования в нашем проекте:

app.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_migrate import Migrate

from flask_login import (
    UserMixin,
    login_user,
    LoginManager,
    current_user,
    logout_user,
    login_required,
)

Мы импортировали:

  • Flask,
  • SQLAlchemy, чтобы помочь нашему приложению Python взаимодействовать с базой данных,
  • Bcrypt для хеширования паролей,
  • Migrate для миграции базы данных,
  • несколько других методов из Flask-Login для управления сеансами.
login_manager = LoginManager()
login_manager.session_protection = "strong"
login_manager.login_view = "login"
login_manager.login_message_category = "info"

Чтобы использовать Flask_login, мы создадим экземпляр, как показано выше. То же самое мы проделаем для SQLAlchemy, Migrate и Bcrypt.

db = SQLAlchemy()
migrate = Migrate()
bcrypt = Bcrypt()

Вместо того, чтобы создавать наш экземпляр Flask глобально (что привело бы к сложностям по мере роста проекта), мы сделаем это внутри функции.

Создавая экземпляр Flask внутри функции, мы сможем использовать несколько экземпляров приложения (в том числе и во время тестирования).

def create_app():
    app = Flask(__name__)

    app.secret_key = 'secret-key'
    app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///database.db"
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

    login_manager.init_app(app)
    db.init_app(app)
    migrate.init_app(app, db)
    bcrypt.init_app(app)
    
    return app

Flask-Login также требует, чтобы мы установили секретный ключ для работы. Кроме того, вы можете заметить, что у нас есть инициализации внутри фабрики приложения. Это для того, чтобы расширения изначально не привязывались к приложению.

Теперь, когда с базовой фабрикой приложения покончено, пора объявить нашу модель User. В таблице пользователей нам нужны только столбцы электронной почты (email), имени пользователя (username) и пароля для этого приложения (password).

[python_ad_block]

models.py

from app import db
from flask_login import UserMixin

class User(UserMixin, db.Model):
    __tablename__ = "user"
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    pwd = db.Column(db.String(300), nullable=False, unique=True)

    def __repr__(self):
        return '<User %r>' % self.username

Мы импортируем db, экземпляр SQLAlchemy и подкласс UserMixin из Flask-Login.

Благодаря UserMixin наша работа упрощается. Он позволяет нам использовать такие методы, как is_authenticated(), is_active(), is_anonymous() и get_id().

Не включив UserMixin в нашу модель User, мы получим ошибки. Например, 'User' object has no attribute 'is_active'.

В настоящее время у нас есть модель User, но мы еще не создали таблицу. Для этого запустите python manage.py в своем терминале в каталоге проекта. Но сначала убедитесь, что вы правильно всё настроили, установили пакеты из файла requirements.txt и имеете активную виртуальную среду.

manage.py

def deploy():
	"""Run deployment tasks."""
	from app import create_app,db
	from flask_migrate import upgrade,migrate,init,stamp
	from models import User

	app = create_app()
	app.app_context().push()
	db.create_all()

	# migrate database to latest revision
	init()
	stamp()
	migrate()
	upgrade()
	
deploy()

Функция deploy импортирует функцию create_app из файла app.py, методы миграции Flask-Migrate и модель User. Затем мы гарантируем, что работаем в контексте приложения, из которого теперь мы можем вызвать db.create all(), который и позаботится о создании нашей таблицы.

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

Чтобы статья была аккуратной и точной, мы опустим строки импорта. Исключенные строки импорта можно посмотреть в репозитории GitHub.

forms.py

Форма регистрации

class register_form(FlaskForm):
    username = StringField(
        validators=[
            InputRequired(),
            Length(3, 20, message="Please provide a valid name"),
            Regexp(
                "^[A-Za-z][A-Za-z0-9_.]*$",
                0,
                "Usernames must have only letters, " "numbers, dots or underscores",
            ),
        ]
    )
    email = StringField(validators=[InputRequired(), Email(), Length(1, 64)])
    pwd = PasswordField(validators=[InputRequired(), Length(8, 72)])
    cpwd = PasswordField(
        validators=[
            InputRequired(),
            Length(8, 72),
            EqualTo("pwd", message="Passwords must match !"),
        ]
    )

В приведенном выше фрагменте кода мы просто применяем проверки к обязательным полям, импортированным из wtforms. Далее мы присваиваем их переменным полей формы.

    def validate_email(self, email):
        if User.query.filter_by(email=email.data).first():
            raise ValidationError("Email already registered!")

    def validate_uname(self, uname):
        if User.query.filter_by(username=username.data).first():
            raise ValidationError("Username already taken!")

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

Форма входа

class login_form(FlaskForm):
    email = StringField(validators=[InputRequired(), Email(), Length(1, 64)])
    pwd = PasswordField(validators=[InputRequired(), Length(min=8, max=72)])
    # Placeholder labels to enable form rendering
    username = StringField(
        validators=[Optional()]
    )

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

routes.py

При использовании Flask-Login важно обеспечить обратный вызов загрузчика пользователя. Это сохраняет текущий пользовательский объект загруженным в текущем сеансе на основе сохраненного идентификатора.

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

В следующих строках кода мы просто определяем три маршрута для нашего приложения: index (для возврата домой), login (войти в систему) и register (зарегистрироваться).

Вы обратили внимание на то, как мы создаем экземпляры формы Flask, а затем передаем их вместе с оператором возврата функции? Мы изменим эти маршруты позже, чтобы улучшить вход в систему и регистрацию. Мы также добавим маршрут для выхода.

# Home route
@app.route("/", methods=("GET", "POST"), strict_slashes=False)
def index():
    return render_template("index.html",title="Home")

# Login route
@app.route("/login/", methods=("GET", "POST"), strict_slashes=False)
def login():
    form = login_form()

    return render_template("auth.html",form=form)

# Register route
@app.route("/register/", methods=("GET", "POST"), strict_slashes=False)
def register():
    form = register_form()

    return render_template("auth.html",form=form)
 
if __name__ == "__main__":
    app.run(debug=True)

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

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

auth.html

<form action="{{ request.path }}" method="POST" class="...">
    
{{ form.csrf_token }}
    
{% with messages = get_flashed_messages(with_categories=true) %}
<!-- Categories: success (green), info (blue), warning (yellow), danger (red) -->
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{category}} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}

{% if request.path == '/register/' %}
{{ form.username(class_="form-control",placeholder="Username")}}
    
{% for error in form.username.errors %}
{{ error }}
{% endfor%}
    
{% endif%}
    
{{ form.email(class_="form-control",placeholder="Email")}}
    
{% for error in form.email.errors %}
{{ error }}
{% endfor%}
    
{{ form.pwd(class_="form-control",placeholder="Password")}}

{% for error in form.pwd.errors %}
{{ error }}
{% endfor%}
    
{% if request.path == '/register/' %}
{{ form.cpwd(class_="form-control",placeholder="Confirm Password")}}
    
{% for error in form.cpwd.errors %}
{{ error }}
{% endfor%}
    
{% endif%}
    
<button type="submit" class="btn btn-block btn-primary mb-3">
{{ btn_action }}
</button>
    
<p>
{% if request.path != '/register/' %}
New here?
<a href="{{url_for('register')}}">Create account</a>
{% else %}
Already have an account?
<a href="{{url_for('login')}}">Login</a>
{% endif %}
</p>

Этот HTML-шаблон служит как формой для входа, так и для регистрации. Мы просто использовали пару приемов для создания шаблонов jinja.

Как видите, для действия формы установлено значение action = "{{request.path}}", где request.path извлекает путь, из которого возник запрос, и назначает его как значение для действия формы. Это избавляет от необходимости жестко прописывать определенные пути.

Мы также устанавливаем переменную токена csrf, которая позволяет продолжить проверку формы, предотвращая межсайтовую подделку запроса.

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

Мы просто выводим имена отдельных переменных из объекта формы для отображения полей формы. Вот пример из приведенного выше фрагмента:

{{form.username (class _ = "form-control", placeholder = "Username")}}

Еще одна вещь, которую следует учитывать, – это использование операторов if...else, например, в следующей строке кода:

{% if request.path == '/ register /'%}

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

Помните проверки, которые мы применяли к полям формы? Мы хотели бы уведомить пользователя, если он введет неверные данные. Поэтому давайте кое-что добавим.

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

{% for error in form.username.errors %}
{{ error }}
{% endfor%}

Выглядеть это может так:

Как изменить routes.py

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

Маршрут регистрации

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

    ...
    
    if form.validate_on_submit():
        try:
            email = form.email.data
            pwd = form.pwd.data
            username = form.username.data
            
            newuser = User(
                username=username,
                email=email,
                pwd=bcrypt.generate_password_hash(pwd),
            )
    
            db.session.add(newuser)
            db.session.commit()
            flash(f"Account Succesfully created", "success")
            return redirect(url_for("login"))

        except Exception as e:
            flash(e, "danger")
            
      ...

Если все проверки пройдены, мы получаем значения из полей формы, которые затем передаются в объект User, добавляются в базу данных, и все изменения сохраняются.

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

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

Хранить пароли в виде обычного текста небезопасно, поскольку это увеличивает риск того, что учетные данные пользователя будут скомпрометированы в случае взлома.

Перед сохранением пароль пользователя хешируется, и в базе данных хранится хорошо зашифрованная комбинация символов. Справиться с этим нам поможет Bcrypt. Хеш создается следующим образом:

pwd = bcrypt.generate_password_hash(pwd)

Маршрут входа

    if form.validate_on_submit():
        try:
            user = User.query.filter_by(email=form.email.data).first()
            if check_password_hash(user.pwd, form.pwd.data):
                login_user(user)
                return redirect(url_for('index'))
            else:
                flash("Invalid Username or password!", "danger")
        except Exception as e:
            flash(e, "danger")

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

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

Выход из системы

@app.route("/logout")
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))

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

Заключение

Вот и все! Мы создали наше приложение с аутентификацией пользователя.

Спасибо за чтение! Мы надеемся, что эта статья была вам полезна.

Перевод статьи «How to Authenticate Users in Flask with Flask-Login».