В „Част 1“ създадохме базовото скеле за готов за производство Flask API. Но какво е API без данни и ако обслужваме чувствителни данни, как да ги защитим? Безпроблемна REST с Flask, част 2 е изцяло за данни и сигурност. Следвайте стъпка по стъпка, докато свързваме нашия API с жива база данни и защитаваме нашите крайни точки. Като напомняне, можете да получите достъп до кода от git repo тук.

Да започваме!

Свързване на база данни

За да опростим нещата, ще използваме Heroku за внедряване на PostgreSQL база данни. Разбира се, вашето приложение може да използва произволен низ за връзка с база данни и не е свързано с Heroku, но Heroku предлага безплатно приложение заедно с безплатна добавка за PostgreSQL. Следвайте тези общи стъпки, за да настроите Heroku:

1. Регистрирайте се за Heroku / Влезте във вашия акаунт в Heroku

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

3. Веднъж създаден, конфигурирайте/добавете добавката „Heroku Postgres“ с плана за разработка на хоби (безплатно)

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, който е дефиниран къде се използва нашият потребителски модел.

Забележете как можем да импортираме нашия потребителски модел в началото на нашата create_app функция и след това по-късно да го използваме в нашия маршрут, за да създадем заявка, която извлича всички потребители. ЗАБЕЛЕЖКА: Ако не сте запознати със синтаксиса List[user], това е синтаксисът за въвеждане на Python с mypy.

Сега, след като имаме дефиниран маршрут и запитван модел, нека изпълним GET заявка, като отидем на „http://127.0.0.1:5000/uhoh“.

Трябва да видите отговор, който гласи: TypeError: Обект от тип потребител не може да се сериализира в 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. До следващия път!