Статьи

Введение в Flask: вход и выход

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

В предыдущей статье мы создали страницу контактов, используя расширения Flask-WTF и Flask-Mail . Мы снова будем использовать Flask-WTF для проверки имени пользователя и пароля пользователя. Мы сохраним эти учетные данные в базе данных, используя еще одно расширение под названием Flask-SQLAlchemy .

Вы можете найти исходный код этого руководства на GitHub . Следуя этому руководству, когда вы видите заголовок, такой как Checkpoint: 13_packaged_app , это означает, что вы можете переключиться на ветку GIT с именем «13_packaged_app» и просмотреть код на этом этапе в статье.


Пока что наше приложение Flask — довольно простое приложение. Он состоит в основном из статических страниц; Итак, мы смогли организовать его как модуль Python. Но теперь нам нужно реорганизовать наше приложение, чтобы его было проще поддерживать и расширять. В документации Flask рекомендуется реорганизовать приложение в виде пакета Python, поэтому давайте начнем с него.

Наше приложение в настоящее время организовано так:

Чтобы реструктурировать его как пакет, давайте сначала создадим новую папку внутри app/ именем intro_to_flask/ . Затем переместите static/ , templates/ , forms.py и routes.py в intro_to_flask/ . Кроме того, удалите все файлы .pyc, которые висят вокруг.

Затем создайте новый файл с именем __init__.py и поместите его в intro_to_flask/ . Этот файл необходим, чтобы Python рассматривал папку intro_to_flask/ как пакет.

Когда наше приложение было модулем Python, импорт и конфигурации для всего приложения были указаны в routes.py Теперь, когда приложение представляет собой пакет Python, мы перенесем эти настройки из routes.py в __init__.py .

app/intro_to_flask/__init__.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
from flask import Flask
 
app = Flask(__name__)
 
app.secret_key = ‘development key’
 
app.config[«MAIL_SERVER»] = «smtp.gmail.com»
app.config[«MAIL_PORT»] = 465
app.config[«MAIL_USE_SSL»] = True
app.config[«MAIL_USERNAME»] = ‘[email protected]
app.config[«MAIL_PASSWORD»] = ‘your-password’
 
from routes import mail
mail.init_app(app)
 
import intro_to_flask.routes

Вершина routes.py теперь выглядит так:

app/intro_to_flask/routes.py

01
02
03
04
05
06
07
08
09
10
from intro_to_flask import app
from flask import render_template, request, flash
from forms import ContactForm
from flask.ext.mail import Message, Mail
 
mail = Mail()
.
.
.
# @app.route() mappings start here

Ранее мы использовали app.run() внутри routes.py , что позволило нам напечатать $ python routes.py для запуска приложения. Поскольку приложение теперь организовано как пакет, нам нужно использовать другую стратегию. Документы Flask рекомендуют добавить новый файл с именем runserver.py и поместить его в app/ . Давайте сделаем это сейчас:

Теперь возьмите вызов routes.py app.run() из routes.py и поместите его в runserver.py .

app/runserver.py

1
2
3
from intro_to_flask import app
 
app.run(debug=True)

Теперь вы можете набрать $ python runserver.py и просмотреть приложение в браузере. Сверху вот как вы войдете в свою среду разработки и запустите приложение:

1
2
3
4
$ cd flaskapp/
$ .
$ cd app/
$ python runserver.py

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

Checkpoint: 13_packaged_app


Мы будем использовать MySQL для нашего механизма базы данных и расширение Flask-SQLAlchemy для управления всем взаимодействием с базой данных.

Flask-SQLAlchemy использует объекты Python вместо операторов SQL для запроса базы данных. Например, вместо того, чтобы писать SELECT * FROM users WHERE firstname = "lalith" , вы должны написать User.query.filter_by(username="lalith").first() .

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

Но почему мы не можем просто написать необработанные операторы SQL? Какой смысл использовать этот странный синтаксис? Как и в большинстве случаев, использование Flask-SQLAlchemy или любого уровня абстракции базы данных зависит от ваших потребностей и предпочтений. Использование Flask-SQLAlchemy позволяет вам работать с вашей базой данных, написав код Python вместо SQL. Таким образом, вы не будете разбрасывать операторы SQL в своем коде Python, и это хорошо с точки зрения качества кода.

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

С другой стороны, это может быть более интуитивно понятным и читаемым, если вы будете писать необработанные операторы SQL вместо того, чтобы учиться переводить их в язык выражений Flask-SQLAlchemy. К счастью, в Flask-SQLAlchemy можно писать необработанные операторы SQL, если это то, что вам нужно.

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

Проверьте, есть ли в вашей системе MySQL, выполнив в терминале следующую команду:

1
$ mysql —version

Если вы видите номер версии, вы можете перейти к разделу «Создание базы данных». Если команда не найдена, вам нужно установить MySQL. Учитывая большое разнообразие различных операционных систем, я обращусь к Google, чтобы предоставить инструкции по установке, которые подходят для вашей ОС. Установка обычно состоит из запуска команды или исполняемого файла. Например, команда Linux:

1
$ sudo apt-get install mysql-server mysql-client

После установки MySQL создайте базу данных для вашего приложения под названием « development ». Вы можете сделать это из веб-интерфейса, такого как phpMyAdmin, или из командной строки, как показано ниже:

1
2
3
4
$ mysql -u username -p
Enter password:
 
mysql> CREATE DATABASE development;

Внутри изолированной среды разработки установите Flask-SQLAlchemy.

1
$ pip install flask-sqlalchemy

Когда я попытался установить Flask-SQLAlchemy, я получил сообщение об ошибке установки. Я искал ошибку и обнаружил, что другие решили эту проблему, установив libmysqlclient15-dev , который устанавливает файлы разработки MySQL. Если ваша установка Flask-SQLAlchemy не удалась, отправьте сообщение об ошибке для решения или оставьте комментарий, и мы постараемся помочь вам разобраться.

Как и в случае с Flask-Mail, нам нужно настроить Flask-SQLAlchemy, чтобы он знал, где находится база данных development . Сначала создайте новый файл с именем models.py вместе с добавлением следующего кода

app/intro_to_flask/models.py

1
2
3
from flask.ext.sqlalchemy import SQLAlchemy
 
db = SQLAlchemy()

Здесь мы импортируем класс SQLAlchemy из Flask-SQLAlchemy (первая строка) и создаем переменную с именем db , содержащую пригодный для использования экземпляр класса SQLAlchemy (третья строка).

Затем откройте __init__.py и добавьте следующие строки после mail.init_app(app) и перед import intro_to_flask.routes .

app/intro_to_flask/__init__.py

1
2
3
4
app.config[‘SQLALCHEMY_DATABASE_URI’] = ‘mysql://your-username:your-password@localhost/development’
 
from models import db
db.init_app(app)

Давайте рассмотрим это:

  1. Первая строка указывает приложению Flask использовать базу данных « development ». Мы указываем это через URI данных, который соответствует шаблону: mysql://username:password@server/database . Сервер «localhost», потому что мы разрабатываем локально. Обязательно введите username и password MySQL.
  2. db , используемый экземпляр класса SQLAlchemy мы создали в models.py , до сих пор не знает, какую базу данных использовать. Поэтому мы импортируем его из models.py ( models.py строка) и привязываем его к нашему приложению (четвертая строка), чтобы он также знал, что нужно использовать базу данных ‘ development ‘. Теперь мы можем запросить базу данных ‘ development ‘ через наш объект db .

Теперь, когда наша конфигурация завершена, давайте убедимся, что все работает. Откройте routes.py и создайте временное сопоставление URL, чтобы мы могли выполнить тестовый запрос.

app/intro-to-flask/routes.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
from intro_to_flask import app
from flask import Flask, render_template, request, flash, session, redirect, url_for
from forms import ContactForm, SignupForm, SigninForm
from flask.ext.mail import Message, Mail
from models import db
.
.
.
@app.route(‘/testdb’)
def testdb():
  if db.session.query(«1»).from_statement(«SELECT 1»).all():
    return ‘It works.’
  else:
    return ‘Something is broken.’

Сначала мы импортируем объект базы данных ( db ) из models.py (строка пять). Затем мы создаем временное сопоставление URL (строки 9-14), в котором мы запускаем тестовый запрос, чтобы убедиться, что приложение Flask подключено к базе данных « development ». Теперь, когда мы /testdb на URL /testdb , /testdb тестовый запрос (строка 11); это эквивалентно оператору SQL SELECT 1; , Если все пойдет хорошо, мы увидим « Это работает » в браузере. В противном случае мы увидим сообщение об ошибке, в котором указано, что пошло не так.

Я получил сообщение об ошибке при посещении URL /testdb : ImportError: No module named MySQLdb . Это означало, что у меня не была установлена ​​библиотека mysql-python, поэтому я попытался установить ее, набрав следующее:

1
$ pip install mysql-python

Эта установка тоже не удалась. В новом сообщении об ошибке предлагается сначала запустить easy_install -U distribute а затем повторить попытку установки mysql-python. Так я и сделал, как показано ниже:

1
2
$ easy_install -U distribute
$ pip install mysql-python

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

Как только тестовый запрос routes.py , удалите временное сопоставление URL-адреса из routes.py Обязательно сохраните часть » from models import db » «, потому что она нам понадобится дальше.

Checkpoint: 14_db_config


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

Внутри базы данных « development » нам нужно создать таблицу пользователей, в которой мы можем хранить информацию о каждом пользователе. Информация, которую мы хотим собирать и хранить, — это имя, фамилия, адрес электронной почты и пароль пользователя.

По соображениям безопасности не рекомендуется хранить пароли в виде простого текста. Если злоумышленник получит доступ к вашей базе данных, он сможет увидеть учетные данные каждого пользователя. Одним из способов защиты от такой атаки является шифрование паролей с помощью хеш-функции и salt (некоторые случайные данные) и сохранение этого зашифрованного значения в базе данных вместо обычного текстового пароля. Когда пользователь снова войдет в систему, мы соберем предоставленный пароль, хешируем его и проверим, соответствует ли он хешу в базе данных. Werkzeug, служебная библиотека, на которой построен Flask, предоставляет функции generate_password_hash и check_password_hash для этих двух задач соответственно.

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

колонка Тип Ограничения
UID ИНТ Первичный ключ, автоинкремент
Имя VARCHAR (100)
фамилия VARCHAR (100)
Эл. адрес VARCHAR (120) уникальный
пароль VARCHAR (54)

Как и раньше, вы можете создать эту таблицу из веб-интерфейса, такого как phpMyAdmin, или из командной строки, как показано ниже:

1
2
3
4
5
6
7
mysql> CREATE TABLE users (
uid INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
firstname VARCHAR(100) NOT NULL,
lastname VARCHAR(100) NOT NULL,
email VARCHAR(120) NOT NULL UNIQUE,
pwdhash VARCHAR(100) NOT NULL
);

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

app/intro_to_flask/models.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from flask.ext.sqlalchemy import SQLAlchemy
from werkzeug import generate_password_hash, check_password_hash
 
db = SQLAlchemy()
 
class User(db.Model):
  __tablename__ = ‘users’
  uid = db.Column(db.Integer, primary_key = True)
  firstname = db.Column(db.String(100))
  lastname = db.Column(db.String(100))
  email = db.Column(db.String(120), unique=True)
  pwdhash = db.Column(db.String(54))
   
  def __init__(self, firstname, lastname, email, password):
    self.firstname = firstname.title()
    self.lastname = lastname.title()
    self.email = email.lower()
    self.set_password(password)
     
  def set_password(self, password):
    self.pwdhash = generate_password_hash(password)
   
  def check_password(self, password):
    return check_password_hash(self.pwdhash, password)

Мы используем функцию set_password () для установки соленого хэша пароля вместо того, чтобы использовать сам текстовый пароль.

Строки одна и четвертая уже существуют в models.py , поэтому мы начнем со второй строки, импортировав функции безопасности generate_password_hash и check_password_hash из Werkzeug. Затем мы создаем новый класс с именем User , унаследованный от объекта базы данных класса Model (шестая строка).

Внутри нашего класса User мы создаем атрибуты для имени таблицы, первичного ключа и имени пользователя, фамилии, адреса электронной почты и пароля (строки 10-14). Затем мы пишем конструктор, который устанавливает атрибуты класса (строки 17-20). Мы сохраняем имена в регистре заголовков и адреса электронной почты в нижнем регистре, чтобы обеспечить совпадение независимо от того, как пользователь вводит свои учетные данные при последующих входах.

Мы используем функцию set_password (строки 22-23) для установки соленого хэша пароля вместо того, чтобы использовать сам текстовый пароль. Наконец, у нас есть функция с именем check_password которая использует check_password_hash , чтобы проверять учетные данные пользователя при любых последующих входах (строки 25-26).

Checkpoint: 15_user_model

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


Посмотрите на Fig. 1 ниже, чтобы увидеть, как все будет сочетаться.

Процесс регистрации.

Fig. 1

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

Давайте рассмотрим фигуру сверху:

  1. Пользователь посещает URL /signup чтобы создать новую учетную запись. Страница извлекается через HTTP-запрос GET и загружается в браузер.
  2. Пользователь заполняет поля формы своим именем, фамилией, адресом электронной почты и паролем.
  3. Пользователь нажимает кнопку «Создать учетную запись», и форма отправляется на сервер с запросом HTTP POST.
  4. На сервере функция проверяет данные формы.
  5. Если одно или несколько полей не проходят проверку, страница регистрации перезагружается с полезным сообщением об ошибке, предлагающим пользователю повторить попытку.
  6. Если все поля действительны, новый объект User будет создан и сохранен в базе данных. Пользователь будет авторизован и перенаправлен на страницу профиля.

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

Мы установили Flask-WTF в предыдущей статье , поэтому давайте приступим к созданию новой формы внутри forms.py .

app/intro_to_flask/forms.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from flask.ext.wtf import Form, TextField, TextAreaField, SubmitField, validators, ValidationError, PasswordField
from models import db, User
.
.
.
class SignupForm(Form):
  firstname = TextField(«First name», [validators.Required(«Please enter your first name.»)])
  lastname = TextField(«Last name», [validators.Required(«Please enter your last name.»)])
  email = TextField(«Email», [validators.Required(«Please enter your email address.»), validators.Email(«Please enter your email address.»)])
  password = PasswordField(‘Password’, [validators.Required(«Please enter a password.»)])
  submit = SubmitField(«Create account»)
 
  def __init__(self, *args, **kwargs):
    Form.__init__(self, *args, **kwargs)
 
  def validate(self):
    if not Form.validate(self):
      return False
     
    user = User.query.filter_by(email = self.email.data.lower()).first()
    if user:
      self.email.errors.append(«That email is already taken»)
      return False
    else:
      return True

Мы начнем с импорта еще одного класса Flask-WTF с именем PasswordField (первая строка), который похож на TextField за исключением того, что он генерирует текстовое поле пароля. Нам понадобится объект базы данных db и модель User для обработки некоторой пользовательской логики валидации внутри класса SignupForm ; поэтому мы импортируем их тоже (строка вторая).

Затем мы создаем новый класс с именем SignupForm содержащий поле для каждой части пользовательской информации, которую мы хотим собрать (строки 7-11). В каждом поле есть валидатор присутствия, чтобы убедиться, что оно заполнено, и валидатор формата, который требует, чтобы адреса электронной почты соответствовали шаблону: [email protected] .

Далее мы пишем простой конструктор для класса, который просто вызывает конструктор базового класса (строки 13-14).

Итак, мы добавили некоторые валидаторы присутствия и формата в наши поля формы, но нам нужен дополнительный валидатор, который гарантирует, что учетной записи с адресом электронной почты пользователя еще не существует. Для этого мы подключаемся к процессу проверки Flask-WTF (строки 16-25).

Теперь внутри функции validate() мы сначала обеспечиваем выполнение валидаторов присутствия и формата, вызывая метод validate() базового класса; если форма не заполнена должным образом, validate() возвращает False (строки 16-17).

Далее мы определяем пользовательский валидатор. Мы начнем с запроса к базе данных по электронной почте, которую отправил пользователь (строка 18). Если вы помните из нашего файла models.py , адрес электронной почты преобразуется в нижний регистр, чтобы обеспечить совпадение независимо от того, как оно было введено. Это выражение Flask-SQLAlchemy соответствует следующему оператору SQL:

1
2
3
SELECT * FROM users
 WHERE email = self.email.data.lower()
 LIMIT 1

Если запись с отправленным письмом уже существует, произойдет сбой проверки с появлением следующего сообщения об ошибке: « Это письмо уже занято » (строки 21-22).

Теперь давайте создадим новое сопоставление URL и новый веб-шаблон для формы регистрации. Откройте routes.py и импортируйте вновь созданную форму регистрации, чтобы мы могли ее использовать.

app/intro_to_flask/routes.py

1
2
3
from intro_to_flask import app
from flask import render_template, request, flash
from forms import ContactForm, SignupForm

Затем создайте новое сопоставление URL.

app/intro_to_flask/routes.py

01
02
03
04
05
06
07
08
09
10
11
12
@app.route(‘/signup’, methods=[‘GET’, ‘POST’])
def signup():
  form = SignupForm()
   
  if request.method == ‘POST’:
    if form.validate() == False:
      return render_template(‘signup.html’, form=form)
    else:
      return «[1] Create a new user [2] sign in the user [3] redirect to the user’s profile»
   
  elif request.method == ‘GET’:
    return render_template(‘signup.html’, form=form)

Внутри функции signup() мы создаем переменную с именем form которая содержит пригодный для использования экземпляр класса SignupForm . Если был отправлен запрос GET, мы signup.html веб-шаблон signup.html содержащий форму регистрации, которую пользователь должен заполнить.

В противном случае мы увидим только строку временного заполнителя. На данный момент в строке temp перечислены три действия, которые должны произойти, когда форма была успешно отправлена. Мы вернемся и заменим эту строку реальным кодом в разделе «Первая регистрация» ниже.

Теперь, когда мы создали сопоставление URL, следующим шагом будет создание веб-шаблона signup.html и его размещение в папке templates/ .

app/intro_to_flask/templates/signup.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{% extends «layout.html» %}
 
{% block content %}
  <h2>Sign up</h2>
 
  {% for message in form.firstname.errors %}
    <div class=»flash»>{{ message }}</div>
  {% endfor %}
   
  {% for message in form.lastname.errors %}
    <div class=»flash»>{{ message }}</div>
  {% endfor %}
   
  {% for message in form.email.errors %}
    <div class=»flash»>{{ message }}</div>
  {% endfor %}
   
  {% for message in form.password.errors %}
    <div class=»flash»>{{ message }}</div>
  {% endfor %}
   
  <form action=»{{ url_for(‘signup’) }}» method=post>
    {{ form.hidden_tag() }}
     
    {{ form.firstname.label }}
    {{ form.firstname }}
     
    {{ form.lastname.label }}
    {{ form.lastname }}
     
    {{ form.email.label }}
    {{ form.email }}
     
    {{ form.password.label }}
    {{ form.password }}
     
    {{ form.submit }}
  </form>
     
{% endblock %}

Этот шаблон выглядит так же, как contact.html . Сначала мы просматриваем и отображаем любые сообщения об ошибках, если это необходимо. Затем мы позволяем Jinja2 генерировать большую часть HTML-формы для нас. Помните, как в классе формы регистрации мы добавили сообщение об ошибке « Это письмо уже занято » к self.email.errors ? Это тот же объект, через который проходит Jinja2 в этом шаблоне.

Единственное отличие от шаблона contact.html — это упущение логики if...else .

В этом шаблоне мы хотим зарегистрироваться и войти в систему после успешной отправки формы. Это происходит в бэкэнде, поэтому здесь не требуется оператор if...else .

Наконец, добавьте эти правила CSS в файл main.css чтобы форма регистрации выглядела красиво и красиво.

app/intro_to_flask/static/css/main.css

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
/* Signup form */
form input#firstname,
form input#lastname,
form input#password {
  width: 400px;
  background-color: #fafafa;
  -webkit-border-radius: 3px;
     -moz-border-radius: 3px;
          border-radius: 3px;
  border: 1px solid #cccccc;
  padding: 5px;
  font-size: 1.1em;
}
 
form input#password {
  margin-bottom: 10px;
}

Давайте проверим недавно созданную страницу регистрации, набрав:

1
2
$ cd app/
$ python runserver.py

И перейдите по адресу http: // localhost: 5000 / signup в вашем любимом веб-браузере.

Страница регистрации.

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

Checkpoint: 16_signup_form

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


Давайте начнем с замены строки временного заполнителя в routes.py signup() routes.py некоторым реальным кодом. После успешной отправки формы нам нужно создать новый объект User , сохранить его в базе данных, войти в систему и перенаправить на страницу профиля пользователя. Давайте сделаем это шаг за шагом, начиная с создания нового объекта User и сохранения его в базе данных.

Добавьте в строки пять и 17-19 к

routes.py.

app/intro_to_flask/routes.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from intro_to_flask import app
from flask import render_template, request, flash, session, url_for, redirect
from forms import ContactForm, SignupForm
from flask.ext.mail import Message, Mail
from models import db, User
.
.
.
@app.route(‘/signup’, methods=[‘GET’, ‘POST’])
def signup():
  form = SignupForm()
   
  if request.method == ‘POST’:
    if form.validate() == False:
      return render_template(‘signup.html’, form=form)
    else:
      newuser = User(form.firstname.data, form.lastname.data, form.email.data, form.password.data)
      db.session.add(newuser)
      db.session.commit()
       
      return «[1] Create a new user [2] sign in the user [3] redirect to the user’s profile»
   
  elif request.method == ‘GET’:
    return render_template(‘signup.html’, form=form)

Сначала мы импортируем класс User из models.py, чтобы мы могли использовать его в функции signup() (строка пять). Затем мы создаем новый объект User именем newuser и заполняем его полевыми данными формы регистрации (строка 17).

Затем мы добавляем newuser в сеанс объекта базы данных (строка 18), который является версией обычной транзакции базы данных Flask-SQLAlchemy. Функция add() генерирует INSERT используя атрибуты объекта User . Эквивалентный SQL для этого выражения Flask-SQLAlchemy:

1
2
INSERT INTO users (firstname, lastname, email, pwdhash)
 VALUES (form.firstname.data, form.lastname.data, form.email.data, form.password.data)

Наконец, мы обновляем базу данных новой пользовательской записью, совершая транзакцию (строка 19).

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

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

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

Flask имеет объект session который выполняет эту функцию. Он сохраняет ключ сеанса в защищенном файле cookie на клиенте и значение сеанса в приложении. Давайте использовать его в нашей функции signup() .

app/intro_to_flask/routes.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import render_template, request, flash, session
.
.
.
@app.route(‘/signup’, methods=[‘GET’, ‘POST’])
def signup():
  form = SignupForm()
   
  if request.method == ‘POST’:
    if form.validate() == False:
      return render_template(‘signup.html’, form=form)
    else:
      newuser = User(form.firstname.data, form.lastname.data, form.email.data, form.password.data)
      db.session.add(newuser)
      db.session.commit()
       
      session[’email’] = newuser.email
       
      return «[1] Create a new user [2] sign in the user [3] redirect to the user’s profile»
   
  elif request.method == ‘GET’:
    return render_template(‘signup.html’, form=form)

Мы начнем с импорта объекта session Flask в первой строке. Затем мы связываем ключ ‘ email ‘ со значением электронной почты вновь зарегистрированного пользователя (строка 17). Объект session позаботится о том, чтобы хэшировать « email » в зашифрованный идентификатор и сохранять его в файле cookie в браузере пользователя. На данный момент пользователь вошел в наше приложение.

Последний шаг — перенаправить пользователя на страницу профиля после url_for в систему. Мы будем использовать функцию url_for (которую мы видели в layout.html и contact.html ) в сочетании с функцией redirect() Flask.

app/intro_to_flask/routes.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
from intro_to_flask import app
from flask import render_template, request, flash, session, url_for, redirect
.
.
.
@app.route(‘/signup’, methods=[‘GET’, ‘POST’])
def signup():
  form = SignupForm()
   
  if request.method == ‘POST’:
    if form.validate() == False:
      return render_template(‘signup.html’, form=form)
    else:
      newuser = User(form.firstname.data, form.lastname.data, form.email.data, form.password.data)
      db.session.add(newuser)
      db.session.commit()
       
      session[’email’] = newuser.email
      return redirect(url_for(‘profile’))
   
  elif request.method == ‘GET’:
    return render_template(‘signup.html’, form=form)

во второй строке мы импортируем функции Flask’s url_for() и redirect() . Затем в строке 19 мы заменяем нашу строку временного заполнителя перенаправлением на URL /profile . У нас пока нет сопоставления URL для /profile , поэтому давайте создадим его следующим.

app/intro_to_flask/routes.py

01
02
03
04
05
06
07
08
09
10
11
12
@app.route(‘/profile’)
def profile():
 
  if ’email’ not in session:
    return redirect(url_for(‘signin’))
 
  user = User.query.filter_by(email = session[’email’]).first()
 
  if user is None:
    return redirect(url_for(‘signin’))
  else:
    return render_template(‘profile.html’)

Здесь мы можем наконец увидеть сессии в действии. Мы начинаем с четвертой строки, выбирая cookie-файл браузера и проверяя, содержит ли он ключ с именем « email ». Если он не существует, это означает, что пользователь не аутентифицирован, поэтому мы перенаправляем пользователя на страницу входа (мы создадим это в следующем разделе).

Если ключ ‘ email ‘ существует, мы ищем значение электронной почты пользователя на стороне сервера, связанное с этим ключом, используя session['email'] , а затем запрашиваем базу данных для зарегистрированного пользователя с тем же адресом электронной почты (строка семь). Эквивалентный SQL для этого выражения Flask-SQLAlchemy:

1
SELECT * FROM users WHERE email = session[’email’];

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

app/intro_to_flask/templates/profile.html

1
2
3
4
5
6
7
{% extends «layout.html» %}
{% block content %}
  <div class=»jumbo»>
    <h2>Profile<h2>
    <h3>This is {{ session[’email’] }}’s profile page<h3>
  </div>
{% endblock %}

Я сохранил этот шаблон профиля простым. Если мы сосредоточимся на пятой строке — вы увидите, что мы можем использовать объект session Flask внутри шаблонов Jinja2. Здесь я использовал ее для создания пользовательской строки, но вы можете использовать эту возможность для извлечения других типов пользовательской информации.

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

1
$ python runserver.py

Перейдите по адресу http: // localhost: 5000 / в своем любимом веб-браузере и завершите процесс регистрации. Вас должна встретить страница профиля, которая выглядит следующим образом:

Успешная регистрация!

Регистрация пользователей является огромной вехой для нашего приложения. Мы можем адаптировать код в нашей функции /signup() и дополнить нашу систему аутентификации, позволяя пользователям входить и выходить из приложения.

Checkpoint: 17_profile_page


Создание страницы входа аналогично созданию страницы регистрации — нам нужно создать форму входа, сопоставление URL-адресов и веб-шаблон. Давайте начнем с создания класса forms.py в forms.py .

app/intro_to_flask/forms.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
class SigninForm(Form):
 email = TextField(«Email», [validators.Required(«Please enter your email address.»), validators.Email(«Please enter your email address.»)])
 password = PasswordField(‘Password’, [validators.Required(«Please enter a password.»)])
 submit = SubmitField(«Sign In»)
  
 def __init__(self, *args, **kwargs):
   Form.__init__(self, *args, **kwargs)
 
 def validate(self):
   if not Form.validate(self):
     return False
    
   user = User.query.filter_by(email = self.email.data.lower()).first()
   if user and user.check_password(self.password.data):
     return True
   else:
     self.email.errors.append(«Invalid e-mail or password»)
     return False

Класс SigninForm аналогичен классу SignupForm . Чтобы войти в систему, нам нужно захватить его электронную почту и пароль, поэтому мы создадим эти два поля с валидаторами присутствия и формата (строки 2-3). Затем мы определяем наш пользовательский валидатор внутри функции validate() (строки 10-15). На этот раз валидатор должен убедиться, что пользователь существует в базе данных и имеет правильный пароль. Если существует запись с предоставленной информацией, мы проверяем, совпадает ли пароль (строка 14). Если это так, проверка проверки проходит (строка 15), в противном случае они получают сообщение об ошибке.

Далее, давайте создадим сопоставление URL-адресов routes.py .

app/intro_to_flask/routes.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
from forms import ContactForm, SignupForm, SigninForm
.
.
.
@app.route(‘/signin’, methods=[‘GET’, ‘POST’])
def signin():
  form = SigninForm()
   
  if request.method == ‘POST’:
    if form.validate() == False:
      return render_template(‘signin.html’, form=form)
    else:
      session[’email’] = form.email.data
      return redirect(url_for(‘profile’))
                 
  elif request.method == ‘GET’:
    return render_template(‘signin.html’, form=form)

Еще раз, signin() похожа на функцию signup() . Мы импортируем SigninForm ( SigninForm строка), чтобы использовать его в функции signin() . Затем в signin() мы возвращаем шаблон signin.html для запросов GET (строки 17-18).

Если форма была отправлена ​​POST и какая-либо проверка не пройдена, форма входа в систему перезагружается с полезным сообщением об ошибке (строки 11-12). В противном случае мы регистрируем пользователя, создавая новый сеанс и перенаправляя на страницу его профиля (строки 14-15).

Наконец, давайте создадим веб-шаблон signin.html .

app/intro_to_flask/templates/signin.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{% extends «layout.html» %}
 
{% block content %}
  <h2>Sign In</h2>
 
  {% for message in form.email.errors %}
    <div class=»flash»>{{ message }}</div>
  {% endfor %}
   
  {% for message in form.password.errors %}
    <div class=»flash»>{{ message }}</div>
  {% endfor %}
   
  <form action=»{{ url_for(‘signin’) }}» method=post>
    {{ form.hidden_tag() }}
     
    {{ form.email.label }}
    {{ form.email }}
     
    {{ form.password.label }}
    {{ form.password }}
     
    {{ form.submit }}
  </form>
     
{% endblock %}

Подобно шаблону signup.html , мы сначала перебираем и отображаем любые сообщения об ошибках, а затем даем Jinja2 сгенерировать форму для нас.

И это делает это для страницы входа. Посетите http: // localhost: 5000 / signin, чтобы проверить это. Идите вперед и войдите, вы должны быть перенаправлены на страницу вашего профиля.

Успешный вход.

Checkpoint: 18_signin_form


В разделе «Первая регистрация» выше мы видели, что «вход в систему» ​​означал установку cookie в браузере пользователя, содержащего идентификатор, и сопоставление этого идентификатора с данными пользователя в приложении Flask. Следовательно, «выход» означает очистку куки в браузере и разобщение пользовательских данных.

Это можно сделать в одной строке: session.pop('email', None) .

Нам не нужна форма или даже веб-шаблон для выхода. Все, что нам нужно, — это сопоставление URL-адреса routes.py , которое завершает сеанс и перенаправляет на домашнюю страницу. Следовательно, отображение короткое и приятное:

app/intro_to_flask/routes.py

1
2
3
4
5
6
7
8
@app.route(‘/signout’)
def signout():
 
  if ’email’ not in session:
    return redirect(url_for(‘signin’))
     
  session.pop(’email’, None)
  return redirect(url_for(‘home’))

Пользователь не аутентифицируется, если cookie-файл браузера не содержит ключ с именем « email », в этом случае мы просто перенаправляем на страницу входа (строки 4-5). В противном случае мы завершаем сеанс (строка семь) и перенаправляем обратно на домашнюю страницу (строка восемь).

Вы можете проверить функциональность выхода, посетив http: // localhost: 5000 / signout . Если вы вошли в систему, приложение выведет вас из системы и перенаправит на http: // localhost: 5000 / . После того, как вы вышли, попробуйте посетить страницу профиля http: // localhost: 5000 / profile . Вам не следует разрешать видеть страницу профиля, если вы не вошли в систему, и приложение должно перенаправить вас обратно на страницу входа.

Checkpoint: 19_signout


Теперь нам нужно обновить заголовок сайта с помощью навигационных ссылок «Зарегистрироваться», «Войти», «Профиль» и «Выйти». Ссылки должны меняться в зависимости от того, вошел ли пользователь в систему или нет. Если пользователь вышел из системы, ссылки для «Регистрация» и «Вход» должны быть видны. Когда пользователь вошел в систему, мы хотим, чтобы появлялись ссылки на «Профиль» и «Выйти», при этом скрываясь ссылки «Регистрация» и «Вход».

Так как мы можем это сделать? Вспомните шаблон profile.html где мы использовали объект session Flask. Мы можем использовать объект session для отображения навигационных ссылок на основе статуса аутентификации пользователя. Давайте откроем layout.html и layout.html следующие изменения:

app/intro_to_flask/templates/layout.html

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html>
  <head>
    <title>Flask App</title>
    <link rel=»stylesheet» href=»{{ url_for(‘static’, filename=’css/main.css’) }}»>
  </head>
  <body>
 
  <header>
    <div class=»container»>
      <h1 class=»logo»>Flask App</h1>
      <nav>
        <ul class=»menu»>
          <li><a href=»{{ url_for(‘home’) }}»>Home</a></li>
          <li><a href=»{{ url_for(‘about’) }}»>About</a></li>
          <li><a href=»{{ url_for(‘contact’) }}»>Contact</a></li>
          {% if 'email' in session %}
          <li><a href="{{ url_for('profile') }}">Profile</a></li>
          <li><a href="{{ url_for('signout') }}">Sign Out</a></li>
          {% else %}
          <li><a href="{{ url_for('signup') }}">Sign Up</a></li>
          <li><a href="{{ url_for('signin') }}">Sign In</a></li>
          {% endif %}
        </ul>
      </nav>
    </div>
  </header>
 
    <div class=»container»>
      {% block content %}
      {% endblock %}
    </div>
 
  </body>
</html>

Начиная со строки 17, мы используем if...elseсинтаксис Jinja2 и session()объект, чтобы проверить, содержит ли cookie браузера emailключ ‘. Если это так, то пользователь вошел в систему и поэтому должен увидеть ссылки навигации «Профиль» и «Выйти». В противном случае пользователь выходит из системы и должен видеть ссылки на «Зарегистрироваться» и «Войти».

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

Последняя оставшаяся задача — похожая проблема: когда пользователь входит в систему, мы не хотим, чтобы он мог посещать страницы регистрации и входа. Для зарегистрированного пользователя не имеет смысла снова проходить аутентификацию. Если вошедшие в систему пользователи пытаются посетить эти страницы, они должны вместо этого быть перенаправлены на страницу своего профиля. Открыть routes.pyи добавьте следующий фрагмент кода в начале signup()и signin()функций:

1
2
if 'email' in session:
 return redirect(url_for('profile'))

Вот как routes.pyбудет выглядеть ваш файл после добавления в этот фрагмент кода:

app/intro_to_flask/routes.py

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@app.route('/signup', methods=['GET', 'POST'])
def signup():
  form = SignupForm()
 
  if 'email' in session:
    return redirect(url_for('profile'))
.
.
.
@app.route('/signin', methods=['GET', 'POST'])
def signin():
  form = SigninForm()
 
  if 'email' in session:
    return redirect(url_for('profile')) 
  …

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

Checkpoint: 20_visibility_control


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

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

  • Разрешите пользователям входить в существующую учетную запись, например в свою учетную запись Google, добавив поддержку OpenID.
  • Дайте пользователям возможность обновлять данные своей учетной записи, а также удалять свою учетную запись.
  • Позвольте пользователям сбросить свой пароль, если они забудут его.
  • Внедрить систему авторизации.
  • Развернуть на рабочем сервере. Обратите внимание, что при развертывании этого приложения в рабочей среде вам необходимо будет внедрить SSL для всего сайта, чтобы пароли и токены сеансов не могли быть перехвачены. При развертывании в Heroku вы можете использовать их SSL-сертификат.

Так что иди и продолжай исследовать Flask и создай свое следующее приложение-убийца! Спасибо за прочтение.