Flask — делаем авторизацию на сайте

Admin Flask

Есть много способов построить авторизацию и административную часть (личный кабинет) на Flask. Некоторые предполагают самостоятельно все строить, другие имеют уже готовые концепции.

Статья входит в цикл статей по разработке на python.

Перед тем как приступить к этой статье следует сделать всё что описано в статье делаем приложение на Flask в локальной среде.

Введение

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

Flask — это микрофреймворк, что предполагает создание проекта по модулям.

Ниже мы построим личный кабинет с авторизацией.

Установка расширений

Flask-Login — расширение, которое обеспечит управление пользовательскими сеансами во Flask. Благодаря этому мы сможем входить и выходить из системы, ограничивать страницы авторизацией.

pip install Flask-Login

Flask-Admin — с помощью этого расширения мы сможем создать личный кабинет на подобии того, что есть в Django.

pip install Flask-Admin

Flask-WTF — расширение для Flask, являющееся оберткой WTForms, с помощью которого можно строить безопасные формы.

Также установим здесь email_validator, библиотеку для проверки эмейлов.

pip install Flask-WTF
pip install email_validator

Flask-Security — разделение пользователей на роли.

pip install Flask-Security

Создаем модели для базы данных

Если ещё не создан, создаем файл models.py внутри которого пишем:

from datetime import datetime
from flask_login import UserMixin
from flask_security import RoleMixin

from app import db, login_manager
from sqlalchemy import ForeignKey
from werkzeug.security import generate_password_hash, check_password_hash


roles_users = db.Table(
    'roles_users',
    db.Column('user_id', db.Integer(), db.ForeignKey('users.id')),
    db.Column('role_id', db.Integer(), db.ForeignKey('roles.id'))
)


class Role(db.Model, RoleMixin):
    __tablename__ = 'roles'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    description = db.Column(db.String(255))

    def __str__(self):
        return self.name


class User(db.Model, UserMixin):
    __tablename__ = 'users'
    id = db.Column(db.Integer, unique=True, primary_key=True)
    name = db.Column(db.String)
    username = db.Column(db.String, unique=True)
    email = db.Column(db.String, unique=True)
    password = db.Column(db.String)
    created_on = db.Column(db.DateTime(), default=datetime.utcnow)
    updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow)
    # Нужен для security!
    active = db.Column(db.Boolean())
    # Для получения доступа к связанным объектам
    roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))

    # Flask - Login
    @property
    def is_authenticated(self):
        return True

    @property
    def is_active(self):
        return True

    @property
    def is_anonymous(self):
        return False

    # Flask-Security
    def has_role(self, *args):
        return set(args).issubset({role.name for role in self.roles})

    def get_id(self):
        return self.id

    # Required for administrative interface
    def __unicode__(self):
        return self.username

    def set_password(self, password):
        self.password = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password, password)


# Отвечает за сессию пользователей. Запрещает доступ к роутам, перед которыми указано @login_required
@login_manager.user_loader
def load_user(user_id):
    return db.session.query(User).get(user_id)

Разберем код.

class Role — класс ролей связанная с таблицей roles.
class User — класс пользователей связанный с таблицей users.
roles_users = db.Table — создание связей между этими двумя таблицами, значение которых будет записываться в новую таблицу roles_users.

@property в виде is_authenticated, is_active, is_anonymous нужны будут для определения авторизованных, активных и анонимных пользователей.

@login_required — пригодится позже для разделения показа страниц на авторизованных и не авторизованных пользователей.

Другие дополнительные участки кода тоже пригодятся или могут пригодиться в дальнейшем.

Запускаем миграцию этих таблиц:

flask db init
flask db migrate -m "Initial migration."
flask db upgrade

Подробнее о миграции таблиц в SQLAlchemy.

Добавляем модуль admin

Создаём директорию admin в директории app со следующими файлами:

Содержимое файла __init__.py:

from app import db, app
from flask import url_for, redirect, request, abort
from app.models import User, UserSetting, Role

# flask-login
from flask_login import current_user
import flask_login as login

# flask-security
from flask_security import SQLAlchemyUserDatastore, Security

# flask-admin
import flask_admin
from flask_admin import helpers, expose
from flask_admin.contrib import sqla

# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)


# Create customized model view class
class MyModelView(sqla.ModelView):

    def is_accessible(self):
        return (current_user.is_active and
                current_user.is_authenticated and
                current_user.has_role('admin')
                )

    def _handle_view(self, name, **kwargs):
        """
        Override builtin _handle_view in order to redirect users when a view is not accessible.
        """

        if not self.is_accessible():
            if current_user.is_authenticated:
                # permission denied
                abort(403)
            else:
                return redirect(url_for('security.login', next=request.url))


# Переадресация страниц (используется в шаблонах)
class MyAdminIndexView(flask_admin.AdminIndexView):
    @expose('/')
    def index(self):
        if not current_user.is_authenticated:
            return redirect(url_for('.login_page'))
        return super(MyAdminIndexView, self).index()

    @expose('/login/', methods=('GET', 'POST'))
    def login_page(self):
        if current_user.is_authenticated:
            return redirect(url_for('.index'))
        return super(MyAdminIndexView, self).index()

    @expose('/logout/')
    def logout_page(self):
        login.logout_user()
        return redirect(url_for('.index'))

    @expose('/reset/')
    def reset_page(self):
        return redirect(url_for('.index'))


# Create admin
admin = flask_admin.Admin(app, index_view=MyAdminIndexView(), base_template='admin/master-extended.html')

# Add view
admin.add_view(MyModelView(User, db.session))


# define a context processor for merging flask-admin's template context into the
# flask-security views.
@security.context_processor
def security_context_processor():
    return dict(
        admin_base_template=admin.base_template,
        admin_view=admin.index_view,
        h=helpers,
        get_url=url_for
    )

Создаем шаблоны для административной части

templates/admin

Создаем внутри директории templates директорию admin. В этой директории добавляем файлы.

master-extended.html

Файлом master-extended.html мы расширяем админ панель. В конкретно данном случае добавляем кнопку выхода на админ панель справа:

Содержимое файла:

{% extends 'admin/base.html' %}
{% block access_control %}

{% if current_user.is_authenticated %}
<div class="btn-group pull-right">
    <a class="btn" href="{{ url_for('admin.logout_page') }}">{{ current_user.username }} - Log out</a>
</div>
{% endif %}
{% endblock %}

index.html

Файл index.html у нас выступит формой входа:

Содержимое файла может быть таким:

{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="row-fluid">

    <div>
        {% if current_user.is_authenticated %}
        <div>
            <h2>Logged in User details</h2>
            <ul>
                <li>Username: {{ current_user.username }}</li>
                <li>Email: {{ current_user.email }}</li>
                <li>Created on: {{ current_user.created_on }}</li>
                <li>Updated on: {{ current_user.updated_on }}</li>
            </ul>

            <h2>Основные ваши настройки</h2>
            <p class="lead">Вы можете:</p>
            <p><a href="/admin/change/">Изменить пароль</a></p>
        </div>

        {% else %}
        <form method="POST" action="">
            {{ form.hidden_tag() if form.hidden_tag }}
            {% for f in form if f.type != 'CSRFTokenField' %}
            <div>
                {{ f.label }}
                {{ f }}
                {% if f.errors %}
                <ul>
                    {% for e in f.errors %}
                    <li>{{ e }}</li>
                    {% endfor %}
                </ul>
                {% endif %}
            </div>
            {% endfor %}
            <button class="btn" type="submit">Submit</button>
        </form>
        {{ link | safe }}
        {% endif %}
    </div>

</div>
{% endblock body %}

templates/security

Когда было установлено расширение Flask-Security в директории templates появилась директория security, а в ней следующие файлы:

Подробно на них останавливаться не будем. Скажу лишь, что эти файлы переопределяют шаблоны расширения Flask-Security.

Шаблоны, которые они расширяют находятся здесь:

/venv/lib/python3.8/site-packages/flask_security/templates/security

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

Config

Для того, чтобы все работало нам нужно добавить данные в файл конфига config-extended. К этому моменты вы должны были ознакомиться с конфигурационными файлами Flask.

Содержимое конфига:

################
# Flask-Security
################

# URLs
SECURITY_URL_PREFIX = "/admin"
SECURITY_LOGIN_URL = "/login/"
SECURITY_LOGOUT_URL = "/logout/"
SECURITY_POST_LOGIN_VIEW = "/admin/"
SECURITY_POST_LOGOUT_VIEW = "/admin/"
SECURITY_POST_REGISTER_VIEW = "/admin/"

# Включает регистрацию
SECURITY_REGISTERABLE = True
SECURITY_REGISTER_URL = "/register/"
SECURITY_SEND_REGISTER_EMAIL = False

# Включет сброс пароля
SECURITY_RECOVERABLE = True
SECURITY_RESET_URL = "/reset/"
SECURITY_SEND_PASSWORD_RESET_EMAIL = True

# Включает изменение пароля
SECURITY_CHANGEABLE = True
SECURITY_CHANGE_URL = "/change/"
SECURITY_SEND_PASSWORD_CHANGE_EMAIL = False

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

Blueprint

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

В файл __init__.py модуля admin нужно добавить:

# Регистрация путей Blueprint
from app.admin.routes import admin_bp
app.register_blueprint(admin_bp, url_prefix="/admin")

Может быть вы обратили внимание, что на скрине выше в модуле admin есть файл routes.py.

Если мы строим большой и расширяемый проект, то у нас в каждом модуле, где требуются роуты, будет свой файл routes.py с путями.

В файл routes.py надо добавить:

from flask import Blueprint
admin_bp = Blueprint('admin_blueprint', __name__)

В этом файле на текущий момент больше ничего не нужно. Все наши роуты прописаны в файле __init__.py, но именно потому что они идут немного в другом формате и переплетены с функционалом. Чтобы все не усложнять мы оставим как есть, но практику применения роутов добавим.

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

Вход в админку

Теперь у нас есть админка. Заходим по адресу:

http://127.0.0.1:5000/admin/login/

Регистрируемся на свой эмейл и заходим внутрь. Наша админка выглядит так (пока видны не все кладки, мы дадим доступ к ним ниже):

Отдельные вкладки для неё мы можем добавлять в файле __init__.py. Например, вот как мы можем добавить новую вкладку для работы с таблицей UserSetting в базе данных (предварительно для неё должна быть создана модель в файле models.py, по инструкции выше):

# Add view
admin.add_view(MyModelView(User, db.session))
admin.add_view(MyModelView(UserSetting, db.session))

Добавим роли для админки. Заходим к себе в БД. Как зайти в локальную ДБ через DataGrip или соединяемся удаленно с базой данных.

В таблице role добавим данные:

И добавим нашему пользователю роль администратора в таблице roles_users:

Если вам пригодилась информация, вы можете поблагодарить автора сайта символическим пожертвованием:

Добавить комментарий

Напишите свой комментарий, если вам есть что добавить/поправить/спросить по теме текущей статьи:
"Flask — делаем авторизацию на сайте"