- •ВВЕДЕНИЕ
- •1. ПОДГОТОВКА К РАЗРАБОТКЕ
- •1.1. НАЧАЛО РАБОТЫ
- •1.2. ИСПОЛЬЗОВАНИЕ КОНТЕЙНЕРОВ DOCKER
- •1.3. НАСТРОЙКА СУБД POSTGRESQL
- •1.4. НАСТРОЙКА ТЕСТОВ
- •1.6. СОЗДАНИЕ RESTFUL-МАРШРУТОВ
- •1.6.1. МАРШРУТ POST /USERS
- •1.6.3. МАРШРУТ GET /USERS
- •1.6.2. МАРШРУТ GET /USERS/:ID
- •1.7. ИСПОЛЬЗОВАНИЕ ШАБЛОНОВ JINJA
- •1.8. СТРУКТУРА ПРОЕКТА
- •2. ОТЛАДКА ПРИЛОЖЕНИЯ
- •2.1. ПОКРЫТИЕ ТЕСТАМИ И КАЧЕСТВО КОДА
- •2.1.1. ПОКРЫТИЕ КОДА ТЕСТАМИ
- •2.1.2. КАЧЕСТВО КОДА
- •2.2. НЕПРЕРЫВНАЯ ИНТЕГРАЦИЯ
- •2.3. ПАНЕЛЬ ИНСТРУМЕНТОВ ОТЛАДКИ FLASK
- •2.4. СТРУКТУРА ПРИЛОЖЕНИЯ
- •3. РАЗВИТИЕ ПРИЛОЖЕНИЯ
- •3.1. ПАКЕТ FLASK MIGRATE
- •3.1.1. ИЗМЕНЕНИЕ МОДЕЛИ
- •3.1.2. НАСТРОЙКА FLASK MIGRATE
- •3.2. РЕФАКТОРИНГ КОДА
- •3.3. ПАКЕТ FLASK BCRYPT
- •3.4. НАСТРОЙКА LOG ROUNDS
- •3.5. ИСПОЛЬЗОВАНИЕ ТОКЕНОВ JWT
- •3.5.1. ПАКЕТ PYJWT
- •3.5.2. РАСШИФРОВКА ТОКЕНА
- •3.6. МАРШРУТЫ ДЛЯ АУТЕНТИФИКАЦИИ
- •3.6.1. МАРШРУТ POST /AUTH/REGISTER
- •3.6.2. МАРШРУТ POST /AUTH/LOGIN
- •3.6.3. МАРШРУТ GET /AUTH/LOGOUT
- •3.6.4. МАРШРУТ GET /AUTH/STATUS
- •ЗАКЛЮЧЕНИЕ
- •СПИСОК ЛИТЕРАТУРЫ
3.4. НАСТРОЙКА LOG ROUNDS
Вы, наверное, заметили, что тесты выполняются намного дольше, чем раньше. Это связано с параметром BCRYPT_LOG_ROUNDS пакета Flask Bcrypt, поскольку мы ещё не определили его значение в конфигурации прило-
жения. Flask Bcrypt использует значение по умолчанию 12, что является из-
лишне большим для тестовой среды. Обновим спецификации теста в файле services/users/project/tests/test_config.py:
class TestDevelopmentConfig(TestCase): def create_app(self):
app.config.from_object('project.config.DevelopmentConfig') return app
def test_app_is_development(self): self.assertTrue(app.config['SECRET_KEY'] == 'secret_key') self.assertFalse(current_app is None)
self.assertTrue( app.config['SQLALCHEMY_DATABASE_URI'] == os.environ.get('DATABASE_URL')
)
self.assertTrue(app.config['DEBUG_TB_ENABLED']) self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 4)
class TestTestingConfig(TestCase): def create_app(self):
app.config.from_object('project.config.TestingConfig') return app
def test_app_is_testing(self): self.assertTrue(app.config['SECRET_KEY'] == 'secret_key') self.assertTrue(app.config['TESTING']) self.assertFalse(app.config['PRESERVE_CONTEXT_ON_EXCEPTION']) self.assertTrue(
app.config['SQLALCHEMY_DATABASE_URI'] == \ os.environ.get('DATABASE_TEST_URL') )
self.assertFalse(app.config['DEBUG_TB_ENABLED']) self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 4)
53
class TestProductionConfig(TestCase): def create_app(self):
app.config.from_object('project.config.ProductionConfig') return app
def test_app_is_production(self): self.assertTrue(app.config['SECRET_KEY'] == 'secret_key') self.assertFalse(app.config['TESTING']) self.assertFalse(app.config['DEBUG_TB_ENABLED']) self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 13)
Убедимся, что тесты завершились неудачно, затем обновим файл
services/users/project/config.py:
# services/users/project/config.py
import os
class BaseConfig:
"""Базовая конфигурация"""
DEBUG = False
TESTING = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = 'secret_key'
BCRYPT_LOG_ROUNDS = 13
class DevelopmentConfig(BaseConfig):
"""Конфигурация для разработки"""
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') BCRYPT_LOG_ROUNDS = 4
class TestingConfig(BaseConfig):
"""Конфигурация для тестирования"""
DEBUG = True
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_TEST_URL') BCRYPT_LOG_ROUNDS = 4
class ProductionConfig(BaseConfig):
54
"""Конфигурация для производственной среды"""
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
Затем обновим файл__init__ в модели User:
class User(db.Model): __tablename__ = "users"
id = db.Column(db.Integer, primary_key=True, autoincrement=True) username = db.Column(db.String(128), unique=True, nullable=False) email = db.Column(db.String(128), unique=True, nullable=False) password = db.Column(db.String(255), nullable=False)
active = db.Column(db.Boolean, default=True, nullable=False)
def __init__(self, username, email, password): self.username = username
self.email = email
self.password = bcrypt.generate_password_hash( password, current_app.config.get('BCRYPT_LOG_ROUNDS')
).decode()
И не забудем про обновление импорта:
from flask import current_app
Запустим тесты ещё раз
1.Тесты выполняются успешно.
2.Тесты выполняются быстрее (0,371 против 4,322 с ранее).
Сделайте коммит, а затем отправьте код на GitHub. Убедитесь, что сборка
Travis успешно создаётся.
3.5.ИСПОЛЬЗОВАНИЕ ТОКЕНОВ JWT
Вэтом параграфе мы добавим пакет JWT в сервис user.
Рабочий процесс аутентификации работает следующим образом.
1. Конечный пользователь отправляет учётные данные для входа с кли-
ента в службу users с помощью технологии AJAX.
2. Затем служба users проверяет правильность учётных данных пользо-
вателя и отвечает токеном аутентификации.
3. Токен хранится на клиенте и отправляется со всеми последующими запросами, а служба users декодирует токен и проверяет его.
55
JWT-токены состоят из трёх основных частей:
1)заголовок;
2)полезная нагрузка;
3)подпись.
3.5.1. ПАКЕТ PYJWT
Для работы с веб-токенами формата JSON в приложении добавим пакет
PyJWT в файл requirements.txt:
pyjwt==1.5.3
Добавим следующий тест в TestUserModel() в файл services/users/project/tests /test_user_model.py:
def test_encode_auth_token(self):
user = add_user('justatest', 'test@test.com', 'test')
auth_token = user.encode_auth_token(user.id)
self.assertTrue(isinstance(auth_token, bytes))
Как всегда, убедимся, что тесты успешно не выполняются, а затем доба-
вим метод encode_auth_token в класс User() файла models.py:
def encode_auth_token(self, user_id):
"""Generates the auth token""" try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=0,\ seconds=5),
'iat': datetime.datetime.utcnow(), 'sub': user_id
}
return jwt.encode( payload,
current_app.config.get('SECRET_KEY'), algorithm='HS256'
)
except Exception as e: return e
Учитывая идентификатор пользователя, метод encode_auth_token кодиру-
ет и возвращает токен. Обратите внимание на полезную нагрузку. Здесь проис-
ходит добавление метаданных о токене и информации о пользователе. Эта ин-
56
формация часто называется «заявлениями» (claims) токенов JWT. Мы исполь-
зовали следующие «заявления»:
1)exp – срок действия токена;
2)iat (issued at) – дата генерации токена;
3)sub – субъект токена, например, – пользователь, которого он иденти-
фицирует.
Добавим следующий импорт:
import datetime
import jwt
Обновим контейнеры:
$ docker-compose -f docker-compose-dev.yml up -d --build
Запустите тесты. Тесты должны выполниться успешно.
Обратимся к конфигурации приложения. Секретный ключ необходимо обновить для производственной среды. Настроим его с помощью переменной окружения.
Сначала в test_config.py изменим все строки вида:
self.assertTrue(app.config['SECRET_KEY'] == 'secret_key')
на:
elf.assertTrue( app.config['SECRET_KEY'] ==
os.environ.get('SECRET_KEY'))
Затем обновим класс BaseConfig в файле services/users/project/config.py:
class BaseConfig:
"""Базовая конфигурация"""
TESTING = False SQLALCHEMY_TRACK_MODIFICATIONS = False SECRET_KEY = os.environ.get('SECRET_KEY') DEBUG_TB_ENABLED = False
DEBUG_TB_INTERCEPT_REDIRECTS = False
BCRYPT_LOG_ROUNDS = 13
Добавим переменную среды SECRET_KEY в среду в файле docker-
compose-dev.yml:
environment:
-FLASK_DEBUG=1
-APP_SETTINGS=project.config.DevelopmentConfig
-DATABASE_URL=postgres://postgres:postgres@usersdb:5432/users_dev
57
-DATABASE_TEST_URL=postgres://postgres:postgres@users- \ db:5432/users_test
-SECRET_KEY=secret_key
Также добавим в конфигурацию срок действия токена:
class BaseConfig:
"""Базовая конфигурация"""
TESTING = False SQLALCHEMY_TRACK_MODIFICATIONS = False SECRET_KEY = os.environ.get('SECRET_KEY') DEBUG_TB_ENABLED = False
DEBUG_TB_INTERCEPT_REDIRECTS = False
BCRYPT_LOG_ROUNDS = 13
TOKEN_EXPIRATION_DAYS = 30
TOKEN_EXPIRATION_SECONDS = 0
class DevelopmentConfig(BaseConfig):
"""Конфигурация для разработки"""
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') DEBUG_TB_ENABLED = True
BCRYPT_LOG_ROUNDS = 4
class TestingConfig(BaseConfig):
"""Конфигурация для тестирования"""
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_TEST_URL') BCRYPT_LOG_ROUNDS = 4
TOKEN_EXPIRATION_DAYS = 0
TOKEN_EXPIRATION_SECONDS = 3
class ProductionConfig(BaseConfig):
"""Конфигурация для производственной среды"""
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
С учётом внесённых изменений необходимо обновить тесты:
class TestDevelopmentConfig(TestCase):
def create_app(self):
app.config.from_object('project.config.DevelopmentConfig')
return app
58
def test_app_is_development(self):
self.assertTrue(
app.config['SECRET_KEY'] == os.environ.get('SECRET_KEY')) self.assertFalse(current_app is None)
self.assertTrue(
app.config['SQLALCHEMY_DATABASE_URI'] ==\ os.environ.get('DATABASE_URL')
)
self.assertTrue(app.config['DEBUG_TB_ENABLED']) self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 4) self.assertTrue(app.config['TOKEN_EXPIRATION_DAYS'] == 30) self.assertTrue(app.config['TOKEN_EXPIRATION_SECONDS'] == 0)
class TestTestingConfig(TestCase): def create_app(self):
app.config.from_object('project.config.TestingConfig') return app
def test_app_is_testing(self): self.assertTrue(
app.config['SECRET_KEY'] == os.environ.get('SECRET_KEY')) self.assertTrue(app.config['TESTING']) self.assertFalse(app.config['PRESERVE_CONTEXT_ON_EXCEPTION']) self.assertTrue(
app.config['SQLALCHEMY_DATABASE_URI'] == \ os.environ.get('DATABASE_TEST_URL')
)
self.assertFalse(app.config['DEBUG_TB_ENABLED']) self.assertTrue(app.config['BCRYPT_LOG_ROUNDS'] == 4) self.assertTrue(app.config['TOKEN_EXPIRATION_DAYS'] == 0) self.assertTrue(app.config['TOKEN_EXPIRATION_SECONDS'] == 3)
class TestProductionConfig(TestCase): def create_app(self):
app.config.from_object('project.config.ProductionConfig') return app
59
