Пишем web на Python3, создаём пользователя приложения

newbie

Опубликован:  2019-02-25T13:07:20.002588Z
Отредактирован:  2019-03-04T07:44:00.721018Z

Продолжаем работу над проектом selfish. У приложения появилась база данных, а это значит, что теперь можно начать последовательное создание объектов этой базы данных и определение для них набора, формата и параметров хранения данных. В этом обзоре я покажу один из возможных вариантов определения пользователя web-приложения и пользовательского аккаунта в рамках оригинальной системы авторизации пользователей.

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

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

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

Реализацию предложенного замысла я начну в этом обзоре с создания модели пользовательского аккаунта и модели пользователя приложения. А поскольку модель пользователя будет содержать дополнительные элементы, определяющие авторизацию и мне понадобятся для этого уже готовые инструменты, первым делом я установлю в виртуальное окружение selfish дополнительные пакеты.

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

cd ~/workspace/selfish
source venv/bin/activate

XMxa5eabZ2.png

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

pip install Flask-Login

Сразу после установки пакета фиксирую изменения виртуального окружения в файле requirements.txt.

pip freeze > requirements.txt

Вновь установленный пакет требует дополнительной конфигурации приложения selfish, открываю в редакторе PyCharm файл config.py и дописываю классу Config ещё одно свойство.

JZeJsvrTiK.png

Открываю файл selfish/__init__.py и модифицирую в этом файле код следующим образом.

f2bpyJXIJL.png

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

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

mkdir selfish/models

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

touch selfish/models/__init__.py

Поскольку в данный момент я работаю над системой авторизации пользователей, создаю для неё файл моделей с именем auth.py.

touch selfish/models/auth.py

И ещё мне необходим модуль, который будет хранить исключения.

touch selfish/models/exc.py

С него я и начну правку кода. Открываю этот файл в редакторе и даю ему два класса исключений.

class MissingArgument(Exception):
    pass


class BadArgument(Exception):
    pass

Эти два класса понадобятся при создании моделей данных. Открываю в редакторе файл selfish/models/auth.py и создаю в нём модель пользовательского аккаунта.

80Zi4cbirS.png

В этом файле я использовал объект базы данных db, который был создан в предыдущем выпуске блога. С его помощью я определил класс Account, дал этому классу необходимые атрибуты. Стоит пояснить, что атрибут ava_hash определяет хеш для сервиса gravatar.com, с его помощью впоследствии будут отображаться аватары пользователей на страницах selfish. Созданный класс имеет метод __init__, определяющий порядок инициализации экземпляров этого класса, в данном случае в этом методе производится проверка переданных аргументов и определён обязательный для создания экземпляра класса аргумент - address. Метод __repr__ определяет возвращаемое экземпляром класса значение, а статический метод on_changed_address будет пересчитывать и изменять значение ava_hash при изменении адреса в экземпляре класса. Последний метод - get_ava_url формирует валидный для сервиса gravatar.com url-адрес.

В последней строке файла объекту db определён обработчик события, который при каждой смене значения атрибута address будет исполнять соответствующий статический метод.

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

kifokxp6vp.png

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

python manage.py db migrate -m"Create Account"

И затем обновляю базу данных.

python manage.py db upgrade

j2BvLmJfJj.png

Отлично... В базе данных появилась первая таблица, можно подключиться в psql и проверить, так ли это.

MzHUE2O8vX.png

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

python manage.py shell

И пробую создать экземпляр класса Account прямо в интерактивной сессии.

ryFwYAHsLz.png

Как видно на снимке экрана выше, я получил возможность создавать аккаунт пользователя, передав классу Account соответствующий адрес электронной почты, а полученный аккаунт даёт возможность хранить, использовать и изменять в случае необходимости учётные данные аккаунта. Изменение адреса электронной почты влечет за собой автоматическое изменение хеша аватара, а аккаунт легко и просто даёт доступ к url-адресу аватара аккаунта в сервисе gravatar.com.

VDEjhF0bJ7.png

Если подключиться к базе данных в psql, то несложно убедиться, что в таблице accounts появилась первая запись.

jFRReqEqJ9.png

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

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

xb1k1lJhun.png

Следует обратить внимание, что класс User наследует от класса UserMixin из пакета flask_login, и от класса db.Model. В атрибутах этого класса содержатся следующие поля данных:

  • id - уникальный идентификатор (primary_key);
  • username - уникальная строка из 16 символов;
  • registered - дата и время регистрации пользователя;
  • last_visit - дата и время последнего визита пользователя в сервис;
  • password_hash - зашифрованный пароль пользователя, с которым он может входить в сервис приложения;
  • account - специальный атрибут, связывающий пользователя с его аккаунтом.

Поскольку определена связь между User и Account, класс Account тоже получил два новых атрибута, определяющих эту связь: user_id и user, таким образом User и Account связаны отношением One-to-One.

Возвращаюсь к классу User и определяю порядок инициализации экземпляров этого класса.

    def __init__(self, **kwargs):
        UserMixin.__init__(self)
        db.Model.__init__(self, **kwargs)
        if 'username' not in kwargs:
            raise MissingArgument('missing required argument: "username"')
        if 'password' not in kwargs:
            raise MissingArgument('missing required argument: "password"')
        if not kwargs['username'] or not isinstance(kwargs['username'], str):
            raise BadArgument('bad required argument: "username"')

Здесь в дандерините я определил два обязательных аргумента: username и password. Но сам класс пока не имеет свойства password - оно будет определено особым способом чуть позже.

Определяю классу User возвращаемое его экземплярами значение.

    def __repr__(self):
        return f"<User: {self.username}>"

Пароль пользователя предполагается хранить в зашифрованном виде в поле password_hash, а доступ к паролю нужно закрыть. Определяю в User ещё одно свойство.

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

Пароль пользователя необходимо установить при регистрации и проверить при входе пользователя в сервис, для этого мне потребуется специальный эквипмент, который предоставляется сервером Werkzeug. Напомню, сервер Werkzeug установился в виртуальное окружение selfish по зависимостям при установке Flask, вписываю в файл selfish/models/auth ещё один импорт.

from werkzeug.security import check_password_hash, generate_password_hash

Возвращаюсь к классу User и дописываю два новых метода.

    @password.setter
    def password(self, password):
        if not password or not isinstance(password, str):
            raise BadArgument('bad required argument: "password"')
        self.password_hash = generate_password_hash(
            password, method='pbkdf2:sha256')

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

Для полного счастья на данном этапе разработки мне потребуется ещё один метод в классе User, который по требованию будет изменять значение свойства last_visit.

    def ping(self):
        self.last_visit = datetime.utcnow()
        db.session.add(self)
        db.session.commit()

Модель пользователя готова.

Кроме авторизованных пользователей сервис selfish должен определять ещё и анонимных пользователей, для этого мне понадобится ещё один импорт в файле selfish/models/auth.py, вписываю его.

from flask_login import AnonymousUserMixin, UserMixin

Здесь же импортирую логин-менеджера.

from .. import db, lm

Теперь я имею возможность создать анонимуса и зарегистрировать его в логин-менеджере.

class AnonymousUser(AnonymousUserMixin):
    pass


lm.anonymous_user = AnonymousUser

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

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

Осталось добавить класс User в контекст приложения в менеджере проекта, открываю файл manage.py, в нём импортирую только что созданный класс User и добавляю его в контекст служебной консоли.

TuHHHk8WzE.png

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

python manage.py db migrate -m"Create User"

Затем обновляю её.

python manage.py db upgrade

1lf8Sp2Vab.png

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

python manage.py shell

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

>>> account = Account.query.get(1)
>>> account.address
'newbie@auriz.ru'
>>>

Создаю пользователя и привязываю его к отфильтрованному аккаунту.

>>> newbie = User(username='newbie', password='abcd')
>>> newbie.account = account
>>> db.session.add(newbie)
>>> db.session.commit()
>>>

Теперь мне хочется взглянуть на пользователя чуть более пристально, начну с возвращаемого значения.

>>> newbie
<User: newbie>
>>>

Меня интересует пароль этого пользователя.

>>> newbie.password
Traceback (most recent call last):
    File "<console>", line 1, in <module>
    File "/home/newbie/workspace/selfish/selfish/models/auth.py", line 37, in password
    raise AttributeError('password is not a readable attribute')
AttributeError: password is not a readable attribute
>>>

Пароль не показывает. Пробую верифицировать разные варианты пароля.

>>> newbie.verify_password('aa')
False
>>> newbie.verify_password('abcd')
True
>>>

Верифицируется только заданный пароль, отлично. Пробую изменить пароль.

>>> newbie.password = 'aa'
>>> db.session.add(newbie)
>>> db.session.commit()
>>> newbie.verify_password('aa')
True
>>> newbie.verify_password('abcd')
False
>>>

Успех. Посмотрим на свойство last_visit и попробуем его изменить с помощью ping.

>>> newbie.last_visit
datetime.datetime(2019, 2, 25, 11, 24, 35, 481268)
>>> newbie.ping()
>>> newbie.last_visit
datetime.datetime(2019, 2, 25, 11, 27, 23, 201319)
>>>

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

>>> newbie.account.address
'newbie@auriz.ru'
>>> newbie.account.get_ava_url()
'http://www.gravatar.com/avatar/f857d4eff9afc8134e785f4e3a9fe2e1?s=100&d=mm&r=g'
>>>

Всё работает... Поскольку User наследует от UserMixin, у пользователя есть свойства и методы определённые в предке.

>>> newbie.is_active
True
>>> newbie.is_anonymous
False
>>> newbie.is_authenticated
True
>>> newbie.get_id()
'1'
>>> 

Эти свойства будут полезны и найдут применение уже на следующей стадии разработки selfish. Стоит подключиться к базе данных в psql и посмотреть, как изменилась база данных.

phNbWBQqYO.png

Подведу промежуточный итог: первые модели данных созданы и заработали, а уже в следующем выпуске блога я покажу, как их можно использовать и реализовать с их помощью логику входа пользователя в сервис. Текущее состояние кода и файлов проекта можно увидеть в моём профиле на gitlab.com. Комментарии приветствуются, вопросы мне можно задать в комментариях, приватным сообщением или на электронную почту. Продолжение следует...

Комментарии: