В Части 1 мы создали базовый каркас для готового к производству Flask API. Но что такое API без данных, и если мы обслуживаем конфиденциальные данные, как нам их защитить? Легкий REST с Flask, часть 2 посвящена данным и безопасности. Следуйте пошаговым инструкциям, пока мы подключаем наш API к действующей базе данных и защищаем наши конечные точки. Напоминаем, что вы можете получить доступ к коду из репозитория git здесь.

Давайте начнем!

Подключение базы данных

Чтобы упростить задачу, мы воспользуемся Heroku для развертывания базы данных PostgreSQL. Конечно, ваше приложение может использовать любую строку подключения к базе данных и не привязано к Heroku, но Heroku предлагает бесплатное приложение вместе с бесплатным надстройкой PostgreSQL. Выполните следующие общие шаги, чтобы настроить Heroku:

1. Зарегистрируйтесь в Heroku / войдите в свою учетную запись Heroku

2. Создайте новое приложение.

3. После создания настройте / добавьте надстройку «Heroku Postgres» с помощью плана Hobby Dev (бесплатно).

4. Добавьте Heroku CLI в свой терминал.

После того, как все настроено, получить строку подключения к базе данных Heroku так же просто, как

heroku config:get DATABASE_URL -a {your_heroku_app_name}

Результатом этой команды будет то, что мы дадим нашему плагину Flask SqlAlchemy в качестве строки подключения. Чтобы упростить эту задачу, добавьте указанную выше команду в tasks.py скрипт, чтобы вы всегда получали DATABASE_URL перед запуском приложения Flask. Вам также нужно будет добавить свой HEROKU_APP_NAME вверху tasks.py.

Подключение к базе данных

Теперь, когда мы можем запустить наш сервер, давайте посмотрим, как мы можем подключить нашу базу данных в chap-2/app/__init__.py.

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

Определение моделей

Что такое база данных без сущностей и моделей? Давайте посмотрим на модель пользователя в app/models/user.py.

"User Model"
# pylint: disable=no-member
from app import db
from sqlalchemy.sql import func

# A generic user model that might be used by an app powered by flask-praetorian
class User(db.Model):
    __tablename__ = "users"
    
    user_id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    roles = db.Column(db.Text)
    is_active = db.Column(db.Boolean, default=True, server_default="true")
    created_datetime = db.Column(
        db.DateTime(), nullable=False, server_default=func.now()
    )

    @property
    def rolenames(self):
        try:
            return self.roles.split(",")
        except Exception:
            return []
    
    @classmethod
    def lookup(cls, username: str):
        return cls.query.filter_by(username=username).one_or_none()
    
    @classmethod
    def identify(cls, user_id: int):
        return cls.query.get(user_id)
    
    @property
    def identity(self):
        return self.user_id
    
    def is_valid(self):
        return self.is_active
    
    def __repr__(self):
        return f"<User {self.user_id} - {self.username}>"

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

Заполнение базы данных

Теперь, когда мы определили нашу строку подключения и определили нашу модель пользователя, давайте заполним нашу базу данных. К счастью, SQLAlchemy предоставляет мощные команды для поддержки базы данных. Эти команды скрыты за командами вызова. Чтобы инициализировать базу данных и заполнить ее, выполните две следующие команды.

invoke init-db
invoke seed-db

Эти две команды определены в tasks.py файле и создают таблицы в нашей базе данных и заполняют их двумя пользователями.

Использование моделей SQLAlchemy для запроса базы данных

В файле chap-2/app/__init__.py есть маршрут / uhoh, который определен там, где используется наша пользовательская модель.

Обратите внимание, как мы можем импортировать нашу модель User в начале нашей функции create_app, а затем использовать ее в нашем маршруте для создания запроса, который выбирает всех пользователей. ПРИМЕЧАНИЕ. Если вы не знакомы с синтаксисом List [user], это синтаксис ввода Python с mypy.

Теперь, когда у нас есть определенный маршрут и запрашиваемая модель, давайте запустим GET-запрос, перейдя на http://127.0.0.1:5000/uhoh.

Вы должны увидеть ответ, в котором говорится: TypeError: объект типа user не является сериализуемым JSON.

Ошибка TypeError сообщает нам, что наш ответ модели SQLAlchemy не является сериализуемым в формате JSON. Другими словами, пользовательскую переменную нельзя так легко преобразовать в JSON. Вместо этого нам нужно десериализовать пользователя и преобразовать его в JSON. Сделать это можно с помощью пакета Marshmallow. Marshmallow - это базовый пакет при выполнении проверки ввода, десериализации в объекты уровня приложения и сериализации объектов уровня приложения в типы Python. Мы настоятельно рекомендуем вам ознакомиться с этим пакетом, поскольку он может предложить вам суперсилы для приложений-объектов, входящих и исходящих из вашего API.

Ниже приведен пример маршрута, который десериализует модель SQLAlchemy в JSON. Он использует определенную схему, чтобы знать, как десериализовать модель SQLAlchemy.

При вызове маршрута с помощью http://127.0.0.1:5000/marsh мы видим, что модели SQLAlchemy правильно преобразовываются в JSON, а веб-браузер отображает JSON.

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

Аутентификация и авторизация

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

В файлеchap-3/app/config.py добавьте конфигурационные переменные JWT_ACCESS_LIFESPAN и JWT_REFRESH_LIFESPAN.

Затем в файле chap-3/app/__init__.py мы можем инициализировать преторианскую библиотеку, добавить маршрут / login и защитить маршруты с помощью специального декоратора @auth_required.

"Main Flask App"
...
# Application Factory
# https://flask.palletsprojects.com/en/1.1.x/patterns/appfactories/
def create_app(config_name: str) -> Flask:
    """Create the Flask application
    Args:
    config_name (str): Config name mapping to Config Class
    Returns:
    [Flask]: Flask Application
    """
    from app.config import config_by_name
    from app.models import User
    # Create the app
    app = Flask(__name__)
    # Log the current config name being used and setup app with the config
    app.logger.debug(f"CONFIG NAME: {config_name}")
    config = config_by_name[config_name]
    app.config.from_object(config)
    # Initialize the database
    db.init_app(app)
    # Initialize the flask-praetorian instance for the app
    guard.init_app(app, User)

    @app.route("/")
    def hello_world() -> str:  # pylint: disable=unused-variable
        return "Hello World!"

    @app.route("/login", methods=["POST"])
    def login():
        # Ignore the mimetype and always try to parse JSON.
        req = request.get_json(force=True)
        username = req.get("username", None)
        password = req.get("password", None)
        user = guard.authenticate(username, password)
        ret = {"access_token": guard.encode_jwt_token(user)}
        return (jsonify(ret), 200)

    @app.route("/users", methods=["GET"])
    @auth_required
    def users():
        users = User.query.all()
        return jsonify(UserSchema(many=True).dump(users))

    @app.route("/users/admin-only", methods=["GET"])
    @roles_required("admin")
    def users_admin():
        users = User.query.all()
        return jsonify(UserSchemaWithPassword(many=True).dump(users))
    
    return app

Мы инициализируем защитную переменную с помощью нашей пользовательской модели SQLAlchemy. Это модель, которую Flask Praetorian будет использовать для аутентификации пользователей при выполнении guard.authenticate () в маршруте / login. Когда пользователь аутентифицирован, мы можем закодировать токен JWT от пользователя и отправить его обратно клиенту. Затем клиент может использовать этот токен при выполнении запросов к бэкэнду, например, в / users route. Декорированный @auth_required проверит заголовки запроса на наличие токена носителя аутентификации. Если токен присутствует, он проанализирует его и проверит пользователя в базе данных. Если пользователь аутентифицирован, маршрут может продолжаться. Создавая готовые к работе приложения, вы всегда должны учитывать, какие данные будут передаваться через приложение. При настройке приложений мы обрабатываем все данные как конфиденциальные, и пользователи должны пройти проверку подлинности. Это отличная передовая практика.

Flask Praetorian также дает нам авторизацию с помощью декоратора @roles_required, как показано в маршруте / users / admin-only. Этот декоратор аутентифицирует пользователя, а затем проверяет наличие у него правильных ролей, прежде чем разрешить продолжение запроса.

Чтобы самостоятельно увидеть эти действия, используйте файл с именем effortless_rest_with_flask_postman.json, который позволяет вам быстро импортировать запросы Postman для работы с вашим локальным API.

Защита вашего приложения может быть невероятно сложной задачей, но Flask Praetorian упрощает ее. С помощью нескольких конфигураций конечных точек вы можете легко добавить аутентификацию и авторизацию. Теперь у вас есть API, подключенный к базе данных, а также маршруты с общей аутентификацией и дополнительной авторизацией. Было бы вполне разумно начать встраивать бизнес-логику в свое приложение прямо сейчас, но нам все еще чего-то не хватает. Как наши потребители API узнают о наших интерфейсах и входах API? На какую документацию они могут положиться, чтобы соответствовать нашему API? В части 3 этой серии мы ответим на эти вопросы с помощью автоматически созданной документации по API в нашем приложении Flask. До скорого!