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

Содержание статьи
Как работает Flask-Login
Flask-Login не хранит пользователей и не проверяет пароли. Эти задачи остаются за вашим кодом и базой данных. Расширение отвечает за другое:
- сохраняет ID пользователя в подписанной сессии после вызова
login_user(); - при каждом запросе загружает пользователя через функцию, помеченную
@login_manager.user_loader; - дает доступ к текущему пользователю через прокси
current_user; - закрывает страницы декоратором
@login_requiredи перенаправляет гостей на форму входа; - по желанию ставит cookie "Запомнить меня" на долгий срок.
Аутентификация отвечает на вопрос "кто вы", авторизация на вопрос "что вам можно". Flask-Login решает первую задачу и дает удобную основу для второй. Если вы только начинаете работать с фреймворком, сначала прочитайте статью про основы Flask.
Установка и настройка LoginManager
Пакет ставится через pip. Вместе с Flask устанавливается Werkzeug, в котором есть функции для хеширования паролей.
pip install flask flask-loginСессия во Flask подписывается секретным ключом, без него вход работать не будет. Ключ должен быть длинной случайной строкой и не попадать в репозиторий, поэтому его читают из переменной окружения.
import os
from flask import Flask
from flask_login import LoginManager
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY', 'dev-only-key')
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login' # имя функции страницы входа
login_manager.login_message = 'Войдите, чтобы открыть эту страницу'Атрибут login_view указывает, куда отправлять неавторизованного посетителя. Если его не задать, защищенная страница вернет ошибку 401. Сгенерировать надежный ключ можно командой python -c "import secrets; print(secrets.token_hex(32))".
Модель пользователя и user_loader
Класс пользователя должен иметь свойства is_authenticated, is_active, is_anonymous и метод get_id(). Писать их вручную не нужно: примесь UserMixin добавляет реализации по умолчанию, достаточно атрибута id.
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
class User(UserMixin):
def __init__(self, id, username, password_hash, is_admin=False):
self.id = id
self.username = username
self.password_hash = password_hash
self.is_admin = is_admin
def check_password(self, password):
return check_password_hash(self.password_hash, password)
# Учебное хранилище, в проекте здесь будет база данных
USERS = {
'1': User('1', 'anna', generate_password_hash('qwerty123'), is_admin=True),
'2': User('2', 'boris', generate_password_hash('boris2024')),
}
@login_manager.user_loader
def load_user(user_id):
return USERS.get(user_id) # None, если пользователя нетВ user_loader приходит строка, то есть то, что вернул get_id(). Если в базе ID целочисленный, преобразуйте его: db.session.get(User, int(user_id)). Функция должна вернуть None для несуществующего пользователя, а не выбрасывать исключение. Как подключить настоящую таблицу пользователей, рассказано в статье про базу данных во Flask.
Вход и выход пользователя
Представление входа проверяет логин и хеш пароля и вызывает login_user(). Параметр remember=True ставит долговременную cookie, по умолчанию она живет 365 дней. Форма здесь простая, в реальном приложении ее удобнее строить через Flask-WTF с защитой от CSRF, это описано в материале про формы Flask.
from urllib.parse import urlsplit
from flask import request, redirect, url_for, flash, render_template_string
from flask_login import login_user, logout_user, login_required, current_user
LOGIN_FORM = '''
<form method="post">
<input name="username" placeholder="Логин">
<input name="password" type="password" placeholder="Пароль">
<label><input type="checkbox" name="remember"> Запомнить</label>
<button>Войти</button>
</form>
'''
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('profile'))
if request.method == 'POST':
name = request.form.get('username', '')
user = next((u for u in USERS.values() if u.username == name), None)
if user and user.check_password(request.form.get('password', '')):
login_user(user, remember='remember' in request.form)
next_page = request.args.get('next')
if not next_page or urlsplit(next_page).netloc:
next_page = url_for('profile')
return redirect(next_page)
flash('Неверный логин или пароль')
return render_template_string(LOGIN_FORM)
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))Обратите внимание на параметр next. Flask-Login добавляет его к адресу страницы входа, чтобы после авторизации вернуть пользователя туда, куда он шел. Значение приходит из URL, поэтому перед редиректом проверяем, что адрес относительный. Иначе злоумышленник может прислать ссылку, которая после входа уведет на чужой сайт.
Пароли нельзя хранить открытым текстом. Функция generate_password_hash() добавляет соль и возвращает строку с алгоритмом и хешем, а check_password_hash() сравнивает введенный пароль с этой строкой.
Защита страниц и проверка ролей
Декоратор @login_required ставится под @app.route. Гость получит редирект на страницу входа, а вошедший пользователь увидит страницу. Внутри представления и в шаблонах Jinja2 доступен current_user.
from functools import wraps
from flask import abort
@app.route('/profile')
@login_required
def profile():
return f'Привет, {current_user.username}!'
def admin_required(view):
@wraps(view)
@login_required
def wrapper(*args, **kwargs):
if not current_user.is_admin:
abort(403)
return view(*args, **kwargs)
return wrapper
@app.route('/admin')
@admin_required
def admin_panel():
return 'Панель администратора'Собственный декоратор admin_required объединяет проверку входа и роли, поэтому его не нужно дублировать в каждом представлении. Без @wraps все обернутые функции получили бы одно имя wrapper, и Flask выдал бы ошибку о повторяющемся endpoint.
В шаблоне удобно показывать разные ссылки гостю и пользователю:
{% if current_user.is_authenticated %}
<a href="{{ url_for('logout') }}">Выйти ({{ current_user.username }})</a>
{% else %}
<a href="{{ url_for('login') }}">Войти</a>
{% endif %}Так шаблон меняет меню в зависимости от статуса посетителя. Подробнее о наследовании шаблонов написано в статье про создание шаблона Flask.
Полезные настройки
Основные параметры, которые меняют поведение расширения:
| Настройка | Где задается | Назначение |
|---|---|---|
login_view | LoginManager | Страница, куда отправляют гостей |
login_message | LoginManager | Текст flash-сообщения при редиректе |
session_protection | LoginManager | Режим "basic" или "strong" для защиты от кражи сессии |
REMEMBER_COOKIE_DURATION | app.config | Срок жизни cookie "Запомнить меня" |
REMEMBER_COOKIE_SECURE | app.config | Передавать cookie только по HTTPS |
Если нужно, чтобы пользователь заново вводил пароль перед опасными действиями, вроде смены почты, используйте декоратор @fresh_login_required. Сессия, восстановленная из cookie "Запомнить меня", не считается свежей. Все параметры перечислены в документации Flask-Login.
Частые вопросы
Ответы на вопросы, которые возникают при подключении Flask-Login.
Почему после входа current_user остается анонимным?
Чаще всего не зарегистрирован user_loader или он возвращает None из-за несовпадения типов: в сессии лежит строка '1', а в словаре или базе ищется число 1. Проверьте также, что задан secret_key.
Как получить 401 вместо редиректа для API?
Не задавайте login_view или зарегистрируйте свой обработчик через @login_manager.unauthorized_handler и верните из него JSON с кодом 401.
Подходит ли Flask-Login для входа по токену?
Да, для этого есть колбэк @login_manager.request_loader. Он получает объект запроса и может найти пользователя по заголовку Authorization или API-ключу.
Как заблокировать пользователя?
Переопределите свойство is_active, чтобы оно возвращало False для заблокированных. Тогда login_user() откажет во входе и вернет False.