Рубрики
Без рубрики

Создание приложения SaaS (часть IV): аутентификация пользователя в колбе и реагировать

После того, как вы закончите этот пост, у вас будет безопасное приложение Flask, которое обрабатывает вход пользователя … Tagged Python, Flask, React.

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

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

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

Вы можете найти полный код на GitHub.

Оглавление

  • Часть I: Создание скребка поиска Google
    • Настройка кукловода на экземпляре AWS
    • Создание простого запроса поиска в Google
    • Использование прокси -сети для запросов скребков
    • Сбор результатов поиска
    • Обработка ошибок скрещин
  • Часть II: Производственное готовое развертывание с Nginx, Flask и Postgres
    • Настройка Docker и Docker Compose
    • Развертывание версии разработки
    • Понимание того, как nginx и колба работают вместе
    • Тестирование Nginx и Flask конфигурация
    • Конфигурация Postgres
    • Настройка SSL с помощью Let’s Encrypt
    • Развертывание производственной версии
  • Часть III: Колба, sqlalchemy и postgres
    • Настройка sqlalchemy и postgres
    • SQLalchemy Performance Performation
    • Настройка нашего первого обработчика маршрута API
  • Часть IV: аутентификация пользователя с помощью колбы и реагировать
    • Защита API REST Flask Rest с помощью JSON Web Tokens
    • Обработка регистрации пользователя в колбе
    • Проверка электронной почты и активация учетной записи
    • Создание пользователя и отправка электронной почты активации
    • Защита страниц в приложении React
    • Добавление Google oauth в качестве опции регистрации

Защита API REST Flask Rest с помощью JSON Web Tokens

Мы будем использовать JWT для аутентификации запросов на API с открытым рангом. JSON Web токены, как следует из названия, является полезной нагрузкой JSON, которая находится либо в файле cookie, либо в локальном хранилище в браузере. Токен отправляется на сервер с каждым запросом API и содержит хотя бы идентификатор пользователя или другую идентифицирующую информацию.

Учитывая, что мы не должны слепо доверять данным, поступающим с фронта, как мы можем доверять тому, что находится внутри JWT? Откуда мы знаем, что кто -то не изменил идентификатор пользователя внутри токена, чтобы выдать себя за другого пользователя?

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

Настройка аутентификации в колбе

Поскольку мы используем маршруты на основе классов через Flask-Restful, мы можем воспользоваться преимуществом наследования, чтобы сделать защиту маршрутов API простым. Маршруты, которые требуют аутентификации, будут наследовать от AuthenticatedView , в то время как общественные маршруты продолжают использовать Ресурс базовый класс.

decode_cookie Функция будет использовать PYJWT для проверки токена и хранить его в глобальном контексте Flask. Мы зарегистрируем функцию декодирования как перед_РЕКОСТА обработчик так, чтобы проверка и хранение токена является первым шагом в жизненном цикле запроса.

from app.services.auth import decode_cookie

def create_app():
    app = Flask(__name__)

    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    app.config["SQLALCHEMY_DATABASE_URI"] = create_db_uri()
    app.config["SQLALCHEMY_POOL_RECYCLE"] = int(
        os.environ.get("SQLALCHEMY_POOL_RECYCLE", 300)
    )

    app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "placeholder_key")
    app.config["SQLALCHEMY_ECHO"] = False

    app.before_request_funcs.setdefault(None, [decode_cookie])

    create_celery(app)
    return app

decode_cookie Функция будет выполняться для каждого запроса, и перед любой логикой обработчика маршрута. Этот шаг только проверяет токен и хранит объект на Г.Куки – Это не аутентифицирует пользователя. Мы увидим, что это произойдет позже в reft_login функция Ниже приведена реализация для decode_cookie функция

import os
import logging
import jwt

from flask import g, request, abort

def decode_cookie():
    cookie = request.cookies.get("user")

    if not cookie:
        g.cookie = {}
        return

    try:
        g.cookie = jwt.decode(cookie, os.environ["SECRET_KEY"], algorithms=["HS256"])
    except jwt.InvalidTokenError as err:
        logging.warning(str(err))
        abort(401)

Поскольку это будет работать по каждому запросу, мы просто возвращаемся рано, если нет cookie. Мы называем функцию Abort с помощью 401, если токен не может проверить, так что фронт-конец React может перенаправить пользователя на страницу входа в систему.

reft_login Функция делает фактическую проверку в базе данных. На этом этапе мы проверили токен и извлекли идентификатор пользователя из этого токена. Теперь нам просто нужно убедиться, что идентификатор пользователя соответствует реальному пользователю в базе данных.

import logging

from flask import make_response, g, abort
from flask_restful import Resource, wraps

from app.models.user import User

def require_login(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        if "id" not in g.cookie:
            logging.warning("No authorization provided!")
            abort(401)

        g.user = User.query.get(g.cookie["id"])

        if not g.user:
            response = make_response("", 401)
            response.set_cookie("user", "")
            return response

        return func(*args, **kwargs)

    return wrapper


class AuthenticatedView(Resource):
    method_decorators = [require_login]

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

Обработка регистрации пользователя в колбе

Для этого проекта я хочу пройти как традиционную запись по электронной почте/пароля, а также использовать Google OAuth. Запустив приложение SaaS, я могу сказать из своего собственного опыта, что оба выполняются хорошо, примерно половина пользователей решили использовать опцию Google OAuth. Добавить, что опция не слишком сложна, и я считаю, что удобство, предлагаемое пользователю, того стоит.

Чтобы начать, давайте посмотрим на Пользователь модель базы данных.

from werkzeug.security import generate_password_hash, check_password_hash
from app import db

class User(db.Model):
    __tablename__ = "user"
    __table_args__ = (db.UniqueConstraint("google_id"), db.UniqueConstraint("email"))

    id = db.Column(db.Integer, primary_key=True)

    # An ID to use as a reference when sending email.
    external_id = db.Column(
        db.String, default=lambda: str(uuid.uuid4()), nullable=False
    )

    google_id = db.Column(db.String, nullable=True)
    activated = db.Column(db.Boolean, default=False, server_default="f", nullable=False)

    # When the user chooses to set up an account directly with the app.
    _password = db.Column(db.String)

    given_name = db.Column(db.String, nullable=True)
    email = db.Column(db.String, nullable=True)
    picture = db.Column(db.String, nullable=True)

    last_login = db.Column(db.DateTime, nullable=True)

    @property
    def password(self):
        raise AttributeError("Can't read password")

    @password.setter
    def password(self, password):
        self._password = generate_password_hash(password)

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

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

Возьмите следующий пример.

user = User()
user.username = "Bob"
user.password = "PasswordForBob"

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

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

Проверка электронной почты и активация учетной записи

Создание новых пользователей с электронной почтой и паролем (в отличие от OAuth) довольно просто. Большая часть работы происходит от проверки адреса электронной почты!

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

Требование шага активации не решает эту проблему на 100%, но это будет иметь большое значение.

Нам понадобится способ для приложения для отправки электронной почты. Я использую API Mailgun для этого проекта и настройка занимает всего несколько минут возиться с DNS Records. После того, как у вас есть учетная запись с Mailgun и правильные записи DNS, отправка электронной почты требует всего несколько шагов.

Во -первых, мы обновим переменные.env и app/ init .py Файлы с необходимыми значениями конфигурации.

MAIL_DOMAIN
MAIL_SENDER
MAILGUN_API_KEY
POSTGRES_USER
POSTGRES_PASSWORD
POSTGRES_HOST
POSTGRES_DB

Если вы помните из более раннего, файл переменных. Новые значения здесь Mail_domain и Mail_sender В Что в моем случае – mail.openranktracker.com и support@openranktracker.com соответственно. Mailgun_api_key Значение используется для аутентификации ваших запросов в API Mailgun.

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

app.config["MAILGUN_API_KEY"] = os.environ["MAILGUN_API_KEY"]
app.config["MAIL_SUBJECT_PREFIX"] = "[OpenRankTracker]"
app.config["MAIL_SENDER"] = os.environ.get("MAIL_SENDER")
app.config["MAIL_DOMAIN"] = os.environ["MAIL_DOMAIN"]

Отправка электронной почты требует одного вызова API в Mailgun. Мы можем использовать модуль запросов, чтобы сделать этот вызов, и мы завершим все это в качестве функции утилиты многократного использования.

def send_email(to, subject, template, **kwargs):
    rendered = render_template(template, **kwargs)

    response = requests.post(
        "https://api.mailgun.net/v3/{}/messages".format(app.config["MAIL_DOMAIN"]),
        auth=("api", app.config["MAILGUN_API_KEY"]),
        data={
            "from": app.config["MAIL_SENDER"],
            "to": to,
            "subject": app.config["MAIL_SUBJECT_PREFIX"] + " " + subject,
            "html": rendered,
        },
    )

    return response.status_code == 201

В отличие от пользовательского интерфейса, который используется с использованием React, мы создадим электронные письма с рендерингом на стороне сервера через шаблоны Jinja. Каталог приложений/шаблонов будет содержать все шаблоны электронной почты, начиная с нашего шаблона проверки электронной почты. Функция send_email принимает дополнительные аргументы ключевых слов, которые затем передаются в render_template, что позволяет нам иметь любые переменные, которые нам нужны, при выполнении шаблона.

app/templates/verify_email.html Сам шаблон очень простой, но функциональный.

Please follow the link below in order to verify your email address!

Verify email and activate account

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

При создании нового шаблона имейте в виду, что большинство почтовых клиентов поддерживают ограниченное подмножество HTML и CSS – проектирование шаблонов электронной почты, даже сегодня, напомнит вам работу с Internet Explorer 6.

Создание пользователя и отправка электронной почты активации

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

Давайте посмотрим на signup.py обработчик маршрута.

from app.services.user import send_email
from app.serde.user import UserSchema
from app.models.user import User
from app import db


class SignUpView(Resource):
    def post(self):
        data = request.get_json()

        user = User.query.filter(
            func.lower(User.email) == data["email"].strip().lower()
        ).first()

        if user:
            abort(400, "This email address is already in use.")

        user = User()
        user.email = data["email"].strip()
        user.password = data["password"].strip()
        user.last_login = datetime.now()

        db.session.add(user)
        db.session.commit()

        send_email(
            user.email,
            "Account activation",
            "verify_email.html",
            root_domain=request.url_root,
        )

        response = make_response("")
        response.set_cookie(
            "user",
            jwt.encode(
                UserSchema().dump(user), app.config["SECRET_KEY"], algorithm="HS256"
            ),
        )

        return response

Это довольно просто, но есть несколько важных «полученных». Когда мы проверяем, уже зарегистрировано электронная почта, мы стараемся сделать случай сравнения нечувствительным и лишить все белое пространство. Другой момент, который нужно помнить здесь, заключается в том, что, хотя мы храним пароль в user.password Плотно-текстовый пароль никогда не хранится навсегда нигде-одностороннее хеширование хранится в _password столбец таблицы.

Ответ, возвращенный клиенту, содержит их новые данные пользователя внутри JWT. Оттуда, фронт будет отправить их на панель панели приложения.

Защита страниц в приложении React

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

Первая проблема заключается в том, как определить, входит ли пользователь в систему или нет. Поскольку мы храним веб-токен JSON в файле cookie, мы будем использовать библиотеку JS-Cookie для обработки поиска cookie, и JWT-Decode, чтобы проанализировать сам токен. Мы выполним проверку в src/app.js, когда страница сначала загружается, чтобы определить, есть ли у пользователя токен.

const App = () => {
    const [loadingApp, setLoadingApp] = useState(true);
    const [loggedIn, setLoggedIn] = useState(false);

    /* 
    ** Check for a user token when the app initializes.
    **
    ** Use the loadingApp variable to delay the routes from
    ** taking effect until loggedIn has been set (even logged in
    ** users would be immediately redirected to login page
    ** otherwise).
    */
    useEffect(() => {
        setLoggedIn(!!getUser());
        setLoadingApp(false);
    }, []);

    return (
        
            {!loadingApp && (
                
                    
                    
                
            )}
        
    );
};

UserContext предоставляется на верхнем уровне приложения, поэтому код в любом месте может определить, входит ли в настоящее время пользователь, и потенциально изменить это состояние. ProtectedRoute Компонент просто завершает другой компонент и предотвращает загрузку этого компонента, если пользователь не вошел в систему, вместо этого отправляет их обратно на страницу входа в систему.

Если мы посмотрим на ProtectedRoute , мы видим, что он использует UserContext Чтобы определить, должен ли он загрузить обернутый компонент или перенаправить на страницу входа в систему.

const ProtectedRoute = ({ component: Component }) => {
    const { loggedIn } = useContext(UserContext);

    return loggedIn ? (
        
    ) : (
        
    );
};

Добавление Google oauth в качестве опции регистрации

В качестве бонуса, теперь мы обратимся к добавлению Google Oauth в качестве опции регистрации и входа в систему. Сначала вам нужно создать учетную запись для доступа Google Developer Console Если вы еще этого не сделали.

После этого вам нужно настроить то, что Google помечает как экран согласия OAuth-это всплывающее окно, которое пользователи увидят, попросят их авторизовать ваше приложение. Этот шаг заполнен предупреждениями о ручных обзорах, но до тех пор, пока вы избегаете каких -либо чувствительных или ограниченных областей (то есть разрешений на учетные записи), ваш экран согласия должен быть немедленно утвержден. Наше приложение требует нечувствительных областей OpenID и электронной почты.

После настройки экрана вашего согласия создайте новый клиент OAuth 2.0 на вкладке учетных данных. Именно здесь вы определите свое авторизованное происхождение и перенаправить URIS, или, другими словами, от того, что процесс OAuth может начать с того, что пользователь должен вернуться после взаимодействия со страницей учетной записи Google.

Это пример моих собственных настроек. Вы также найдете свой идентификатор клиента и секрет на этой странице.

Google_client_id и Google_client_secret Окружающая среда должна будет найти свой путь в переменные.env так что контейнер приложений может забрать их.

Приложение Flask имеет 4 отдельные конечные точки, которые обрабатывают поток OAuth. Обработчики маршрута содержатся в oauthsignup.py и oauthlogin.py очень просты и просто перенаправьте браузер в Google, генерируя URL -адрес обратного вызова. Фронт-энд React сделает подчинение формы к одному из них, в результате чего браузер покинул наше заявление.

from flask import request, redirect
from flask_restful import Resource

from app.services.auth import oauth2_request_uri


class Oauth2SignUpView(Resource):
    def post(self):
        return redirect(
            oauth2_request_uri(request.url_root + "api/users/oauth2callback/signup/")
        )

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

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

Это обработчик маршрута регистрации, который будет выполняться, когда Google перенаправляет браузер обратно в наш домен.

from app.services.auth import get_user_info
from app.serde.user import UserSchema
from app.models.user import User
from app import db


class Oauth2SignUpCallbackView(Resource):
    def get(self):
        oauth_code = request.args.get("code")

        userinfo = get_user_info(oauth_code)
        google_id = userinfo["sub"]

        # Find existing authenticated Google ID or an existing email that the
        # user previously signed up with (they're logging in via Google for
        # the first time).
        user = User.query.filter(
            or_(
                User.google_id == google_id,
                func.lower(User.email) == userinfo["email"].lower(),
            )
        ).first()

        if not user:
            user = User()

        user.google_id = google_id
        user.given_name = userinfo["given_name"]
        user.email = userinfo["email"]
        user.last_login = datetime.now()
        user.activated = True

        db.session.add(user)
        db.session.commit()

        response = redirect(request.url_root)
        response.set_cookie(
            "user",
            jwt.encode(
                UserSchema().dump(user), app.config["SECRET_KEY"], algorithm="HS256"
            ),
        )

        return response

get_user_info Функция утилиты объединяет код OAuth, возвращаемый из Google с нашим идентификатором клиента и секретом, чтобы получить нечувствительные данные об пользователе, включая адрес электронной почты и заданное имя.

Обработчик маршрута также проверяет базу данных для существующего пользователя, просто чтобы убедиться, что мы не создаем новых пользователей, когда существующий пользователь снова зарегистрируетесь по любой причине. Я также решил синхронизировать пользователей, не являющихся OAUTH с их идентификатором Google, если они должны нажать «Зарегистрироваться в Google» после прохождения традиционного процесса регистрации.

Помните, что Весь код на GitHub Если вы хотите использовать этот проект в качестве примера для настройки OAuth в своем собственном приложении.

Что дальше?

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

Оригинал: “https://dev.to/zchtodd_79/building-a-saas-app-part-iv-user-authentication-in-flask-and-react-54fc”