Как стать автором
Обновить

Мега-Учебник Flask, Часть 5: Пользовательские логины (издание 2018)

Время на прочтение15 мин
Количество просмотров104K

blog.miguelgrinberg.com


Miguel Grinberg




<<< предыдущая следующая >>>


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




Это пятый выпуск серии Flask Mega-Tutorial, в котором я расскажу вам, как создать подсистему входа пользователя.


Для справки ниже приведен список статей этой серии.



Примечание 1: Если вы ищете старые версии данного курса, это здесь.


Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.


В главе 3 вы узнали, как создать форму входа пользователя, а в главе 4 вы узнали, как работать с базой данных. В этой главе вы узнаете, как объединить темы из этих двух глав, чтобы создать простую систему входа пользователя.


Ссылки GitHub для этой главы: Browse, Zip, Diff.


Хеширование паролей


В главе 4 пользовательской модели было присвоено поле password_hash, которое пока не используется. Цель этого поля — сохранить хэш пароля пользователя, который будет использоваться для проверки пароля, введенного пользователем во время процесса регистрации. Хеширование паролей ( Password hashing ) — это сложная тема, которую следует оставить экспертам по безопасности, но есть несколько простых в использовании библиотек, которые реализуют всю эту логику таким образом, чтобы ее можно было вызвать из приложения.


Одним из пакетов, реализующих хеширование паролей, является Werkzeug, который вы, возможно, видели в выводе pip при установке Flask. Раз уж это зависимость, Werkzeug уже установлен в вашей виртуальной среде. Следующий сеанс оболочки Python демонстрирует, как хешировать пароль:


>>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash('foobar')
>>> hash
'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f'

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


Процесс проверки выполняется со второй функцией от Werkzeug следующим образом:


    >>> from werkzeug.security import check_password_hash
    >>> check_password_hash(hash, 'foobar')
    True
    >>> check_password_hash(hash, 'barfoo')
    False

Функция проверки принимает хэш-код пароля, который был ранее сгенерирован, и пароль, введенный пользователем во время входа в систему. Функция возвращает значение True, если пароль, предоставленный пользователем, совпадает с хешем, иначе False.


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


app/models.py: Хеширование и проверка пароля

from werkzeug.security import generate_password_hash, check_password_hash

# ...

class User(db.Model):
    # ...

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

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

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


>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('mypassword')
>>> u.check_password('anotherpassword')
False
>>> u.check_password('mypassword')
True

Введение в Flask-Login


В этой главе я познакомлю вас с очень популярным расширением Flask под названием Flask-Login. Это расширение управляет состоянием входа пользователя в систему, так что, например, пользователи могут войти в приложение, а затем перейти на разные страницы, пока приложение «помнит», что пользователь вошел в систему. Оно также предоставляет функциональность «запомнить меня», которая позволяет пользователям оставаться в системе даже после закрытия окна браузера. Чтобы быть готовым к этой главе, вы можете начать с установки Flask-Login в вашей виртуальной среде:


(venv) $ pip install flask-login

Как и с другими расширениями, Flask-Login должен быть создан и инициализирован сразу после экземпляра приложения в app/__init__.py. Так инициализируется это расширение:


app/__init__.py: Flask-Login initialization

# ...
from flask_login import LoginManager

app = Flask(__name__)
# ...
login = LoginManager(app)

# ...

Подготовка User Model для Flask-Login


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


Ниже перечислены четыре обязательных элемента:


  • is_authenticated: свойство, которое имеет значение True, если пользователь имеет действительные учетные данные или False в противном случае.
  • is_active: свойство, которое вернет True, если учетная запись Пользователя активна или False в противном случае.
  • is_anonymous: свойство, которое вернет False для обычных пользователей, и True, если пользователь анонимный.
  • get_id(): метод, который возвращает уникальный идентификатор пользователя в виде строки (unicode, если используется Python 2).

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


app/models.py: Flask-Login user mixin class

# ...
from flask_login import UserMixin

class User(UserMixin, db.Model):
    # ...

Пользовательский загрузчик


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


Поскольку Flask-Login ничего не знает о базах данных, ему нужна помощь приложения при загрузке пользователя. По этой причине расширение ожидает, что приложение настроит функцию загрузчика пользователя, которую можно вызвать для загрузки пользователя с идентификатором. Эта функция может быть добавлена ​​в модуле app/models.py:


app/models.py: Flask-Login user loader function

from app import login
# ...

@login.user_loader
def load_user(id):
    return User.query.get(int(id))

Пользовательский загрузчик зарегистрирован в Flask-Login с помощью декоратора @login.user_loader. Идентификатор, который Flask-Login переходит к функции в качестве аргумента, будет строкой, поэтому для баз данных, использующих числовые идентификаторы, необходимо преобразовать строку в целое число, как вы видите выше int(id).


Вход пользователей в систему


Давайте перейдем к функции входа в систему, которая, как вы помните, реализовала поддельный логин, который только выдавал сообщение flash(). Теперь, когда приложение имеет доступ к пользовательской базе данных и знает, как создавать и проверять хэши паролей, эта функция просмотра может быть завершена ( \microblog\app\routes.py ).


app/routes.py: Login view function logic

# ...
from flask_login import current_user, login_user
from app.models import User

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title='Sign In', form=form)

Две верхние строчки в функции login() приводят к странной ситуации. Представьте, что у вас есть пользователь, который вошел в систему и желая перейти к URL-адресу /login вашего приложения заново попадает на страницу авторизации. Ясно, что это ошибка, поэтому я не хочу этого допускать. Переменная current_user поступает из Flask-Login и может использоваться в любое время для получения объекта пользователя. Значение этой переменной может быть пользовательским объектом из базы данных (который Flask-Login читает через обратный вызов загрузчика пользователя, представленный выше), или специальный анонимный пользовательский объект, если пользователь еще не входил в систему. Помните те свойства, которые требуются Flask в пользовательском объекте? Один из них был is_authenticated, что очень полезно, чтобы проверить, зарегистрирован ли пользователь или нет. Когда пользователь уже вошел в систему, я просто перенаправляю его на страницу index.


Вместо вызова flash(), который я использовал ранее, теперь я могу войти в систему пользователя по-настоящему. Первым шагом является загрузка пользователя из базы данных. Имя пользователя пришло с формой отправки, так что я могу запросить базу данных, чтобы найти пользователя.


Для этого я использую метод filter_by() объекта запроса SQLAlchemy. Результат filter_by() — это запрос, который включает только объекты, у которых есть совпадающее имя пользователя. Поскольку я знаю, что будет только один или нулевой результат, я завершу запрос, вызвав first(), который вернет объект пользователя, если он существует, или None, если это не так. В главе 4 вы видели, что когда вы вызываете метод all() в запросе, запрос выполняется, и вы получаете список всех результатов, соответствующих этому запросу. Метод first() является другим используемым способом выполнения запроса, когда вам нужен только один результат.


Если я получил соответствие для имени пользователя, которое было предоставлено, я могу проверить, действительно ли пароль, который также пришел с формой, действителен. Это делается путем вызова метода check_password(), определенного выше. Это приведет к хеш-паролю, хранящемуся у пользователя, и определит, соответствует ли введенный в форму пароль хешу или нет. Итак, теперь у меня есть два возможных условия ошибки: имя пользователя может быть недопустимым, или пароль может быть неправильным для пользователя. В любом из этих случаев я прокручиваю сообщение и перенаправляю обратно в приглашение для входа, чтобы пользователь мог попробовать еще раз.


Если имя пользователя и пароль верны, я вызываю функцию login_user(), которая поступает из Flask-Login. Эта функция будет регистрировать пользователя во время входа в систему, поэтому это означает, что на любых будущих страницах, к которым пользователь переходит, будет установлена ​​переменная current_user для этого пользователя.


Чтобы завершить процесс входа в систему, я просто перенаправляю вновь зарегистрированного пользователя на страницу index.


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


Очевидно, что нужно будет предложить пользователям возможность выхода из приложения. Это можно сделать с помощью функции logout_user() Flask-Login. Вот как выглядит функция выхода:


app/routes.py: Logout view function

# ...
from flask_login import logout_user

# ...

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))

Я могу заставить ссылку «login» на панели навигации автоматически переключиться на ссылку «logout» после входа пользователя в систему. Это можно сделать с помощью условного выражения в шаблоне base.html:


app/templates/base.html: Conditional login and logout links

<div>
    Microblog:
    <a href="{{ url_for('index') }}">Home</a>
    {% if current_user.is_anonymous %}
    <a href="{{ url_for('login') }}">Login</a>
    {% else %}
    <a href="{{ url_for('logout') }}">Logout</a>
    {% endif %}
</div>

Свойство is_anonymous является одним из атрибутов, которые Flask-Login добавляет к объектам пользователя через класс UserMixin. Выражение current_user.is_anonymous вернет True, только если пользователь не войдет в систему.


Требование к пользовательскому входу


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


Чтобы эта функция была реализована, Flask-Login должен знать, что такое функция просмотра, которая обрабатывает логины. Это можно добавить в app/ init.py:


# ...
login = LoginManager(app)
login.login_view = 'login'

Значение «login» выше является именем функции (или конечной точки) для входа в систему. Другими словами, имя, которое вы будете использовать в вызове url_for(), чтобы получить URL.


Способ Flask-Login защищает функцию просмотра от анонимных пользователей с помощью декоратора, называемого @login_required. Когда вы добавляете этот декоратор к функции вида под декораторами @app.route из Flask, функция становится защищенной и не разрешает доступ к пользователям, которые не аутентифицированы. Вот как декоратор может быть применен к функции просмотра индексов приложения:


app/routes.py: @login_required decorator

from flask_login import login_required

@app.route('/')
@app.route('/index')
@login_required
def index():
    # ...

Остается реализовать перенаправление с успешного входа на страницу, к которой пользователь хотел получить доступ. Когда пользователь, не входящий в систему, обращается к функции просмотра, защищенной декодером @login_required, декоратор собирается перенаправить на страницу входа в систему, но в это перенаправление будет включена дополнительная информация, чтобы приложение затем могло вернуться к первой странице. Если пользователь переходит, например на /index, обработчик @login_required перехватит запрос и ответит перенаправлением на /login, но он добавит аргумент строки запроса к этому URL-адресу, сделав полный URL /login?Next = /index. next аргумент строки запроса установлен на исходный URL-адрес, поэтому приложение может использовать это для перенаправления после входа в систему.


Вот фрагмент кода, который показывает, как читать и обрабатывать next-аргумент строки запроса:


app/routes.py: Перенаправление на "next" (следующую) страницу

from flask import request
from werkzeug.urls import url_parse

@app.route('/login', methods=['GET', 'POST'])
def login():
    # ...
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('index')
        return redirect(next_page)
    # ...

Сразу после того, как пользователь выполнил вход, вызвав функцию login_user() из Flask-Login, вы получите значение next-аргумента строки запроса. Flask содержит переменную запроса, содержащую всю информацию, которую клиент отправил с запросом. В частности, атрибут request.args предоставляет содержимое строки запроса в формате дружественного словаря. На самом деле существует три возможных случая, которые необходимо учитывать, чтобы определить, где перенаправить после успешного входа в систему:


  • Если URL-адрес входа не имеет следующего аргумента, пользователь перенаправляется на индексную страницу.
  • Если URL-адрес входа включает аргумент next, который установлен в относительный путь (или, другими словами, URL-адрес без части домена), тогда пользователь перенаправляется на этот URL-адрес.
  • Если URL-адрес входа включает аргумент next, который установлен на полный URL-адрес, который включает имя домена, то пользователь перенаправляется на страницу индекса.

Первый и второй случаи не требуют пояснений. Третий случай заключается в том, чтобы сделать приложение более безопасным. Злоумышленник может вставить URL-адрес на злоумышленный сайт в аргумент next, поэтому приложение перенаправляет только URL-адрес, что гарантирует, что перенаправление останется на том же сайте, что и приложение. Чтобы определить, является ли URL относительным или абсолютным, я анализирую его с помощью функции url_parse() Werkzeug, а затем проверяю, установлен ли компонент netloc или нет.


Отображение вошедшего в систему пользователя в шаблонах


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


app/templates/index.html: Передаём текущего пользователя в шаблон

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ current_user.username }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {% endfor %}
{% endblock %}

И я могу удалить аргумент user в функции view ( microblog\app\routes.py ):


app/routes.py: Do not pass user to template anymore

@app.route('/')
@app.route('/index')
def index():
    # ...
    return render_template("index.html", title='Home Page', posts=posts)

Кажется наступил подходящий момент для проверки работоспособности входа и выхода. Поскольку регистрация пользователей по-прежнему отсутствует, единственный способ добавить пользователя в базу данных — сделать это через оболочку Python, поэтому запустите flask shell и введите следующие команды для регистрации пользователя:


>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()

Если вы запустите приложение и попытаетесь получить доступ к http:// localhost:5000/ или http://localhost:5000/index, вы будете немедленно перенаправлены на страницу входа в систему. И после завершения процедуры входа в систему, используя учетные данные пользователя, который вы добавили в свою базу данных, вы будете возвращены на исходную страницу, в которой вы увидите персонализированное приветствие.


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


Последняя часть функциональности, которую я собираюсь построить в этой главе, — это форма регистрации, чтобы пользователи могли зарегистрироваться через веб-форму. Начнем с создания класса веб-формы в app/forms.py:


from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User

# ...

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('Please use a different username.')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')

В этой новой форме есть несколько интересных вещей, связанных с проверкой. Во-первых, для поля электронной почты email я добавил второй валидатор после DataRequired, называемый Email. Это еще один валидатор (в оригинале «stock validator», т.е. правильнее это перевести как встроенный, стандартный), который поставляется с WTForms, который гарантирует, что то, что пользователь вводит в этом поле, соответствует структуре адреса электронной почты.


Поскольку это форма регистрации, обычно принято запрашивать у пользователя два раза ввести пароль, чтобы уменьшить риск опечатки. По этой причине у меня есть password и password2. Во втором поле пароля используется еще один стандартный валидатор EqualTo, который проверяет, что его значение идентично значению для первого поля пароля.


Я также добавил к этому классу два метода: validate_username() и validate_email(). Когда вы добавляете какие-либо методы, соответствующие шаблону validate_<имя_поля>, WTForms принимает их как пользовательские валидаторы и вызывает их в дополнение к стандартным валидаторам. В этом случае я хочу убедиться, что имя пользователя и адрес электронной почты, введенные пользователем, еще не находятся в базе данных, поэтому эти два метода выдают запросы к базе данных, ожидая, что результатов не будет. В случае, если результат существует, ошибка проверки инициируется вызовом ValidationError. Сообщение, включенное в качестве аргумента в исключение, будет сообщением, которое будет отображаться рядом с полем для просмотра пользователем.


Чтобы отобразить эту форму на веб-странице, мне нужно иметь HTML-шаблон, который я собираюсь хранить в файле app/templates/register.html. Этот шаблон построен так же, как и для формы входа:


{% extends "base.html" %}

{% block content %}
    <h1>Register</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.email.label }}<br>
            {{ form.email(size=64) }}<br>
            {% for error in form.email.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password2.label }}<br>
            {{ form.password2(size=32) }}<br>
            {% for error in form.password2.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

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


<p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>

И, наконец, мне нужно написать функцию просмотра, которая будет обрабатывать регистрацию пользователей в app/routes.py:


from app import db
from app.forms import RegistrationForm

# ...

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!')
        return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)

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



<<< предыдущая следующая >>>

Теги:
Хабы:
Всего голосов 10: ↑9 и ↓1+8
Комментарии7

Публикации

Истории

Работа

Python разработчик
121 вакансия
Data Scientist
78 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань