...

Регистрация, аутентификация и авторизация: проектирование в UML-sequence и Python-реализация REST API интернет-магазина

Проектирование и реализация REST API для аналитика пример, Flask REST API аутентификация пример, как спроектировать аутентификацию REST API, архитектура информационных систем примеры курсы обучение, системный анализ на пальцах простой пример для начинающих, техники бизнес-анализа, обучение аналитиков, техники бизнес-анализа для начинающих, UML Use Case примеры, обучение начинающих аналитиков, курсы бизнес-анализа, курсы системного анализа, UML-Диаграмма аутентификация авторизация регистрация, Школа прикладного бизнес-анализа Учебный центр Коммерсант

Недавно я рассказывала про аутентификацию в веб-приложениях с помощью JWT-токена. Сегодня рассмотрим практическую реализацию регистрации пользователей и аутентификацию клиентов на сервере с помощью куки-файлов. Как обычно, в качестве примера возьмем интернет-магазин, представляющий собой серверное Flask-приложение, запущенное в Google Colab и тунеллированное с помощью утилиты ngrok.

Регистрация пользователя: проектирование и реализация

На основе проектирования модели данных и REST API серверного приложения, выполненного в прошлой статье, рассмотрим, как именно реализовать ролевое разделение вариантов использования между акторами системы. Например, в области управления товарами вариант использования UC 1.1 Посмотреть список  товаров доступен для всех пользователей, добавление нового товара, изменение или удаление существующего возможно только для Менеджеров магазина. Необходимость аутентификации как системного варианта использования показывается на UML-диаграмме Use Case с помощью связи include. Например, пользователь с ролью Менеджер может добавить товар, что включает его аутентификацию, т.е. проверку того, что этот пользователь действительно является менеджером.

Управление товарами в интернет-магазине, UML-диаграмма Use Case, UML для аналитика пример
Управление товарами в интернет-магазине

Напомню, процедуре аутентификации пользователя предшествует его регистрация. Для этого в модели данных проектируемой системы реализована таблица users. Диаграмма последовательности выглядит так:

UML, системный анализ, регистрация пользователя диаграмма последовательности пример
UML-Диаграмма последовательности регистрации нового пользователя в системе

Скрипт PlantUML для этой диаграммы:

@startuml
title: Регистрация пользователя в системе
actor User
participant Серверное_Приложение as System
database БД
User -> System: зарегистрироваться() через GET-запрос к маршруту /registration
System --> User: страница регистрации
User -> System: отправить данные для регистрации(login, password, role) через POST-запрос к маршруту /registration
System -> System: Проверить валидность данных()
alt данные валидны
System -> БД: узнать количество строк в таблице users для вычисления id()
БД -> БД: SELECT COUNT(id) FROM users
БД --> System: количество строк в таблице users
System -> System: сгенерировать id для новой записи(количество строк в таблице users + 1)
System -> System: создать объект для записи в БД(id, login, password, role)
System -> БД: создать нового пользователя(id, login, password, role) через INSERT-запрос к таблице users
БД -> БД: INSERT INTO users (id, login, password, role) VALUES (id, login, password, role)
БД --> System: запись создана
System -> System: создать объект пользователь(user)
System --> User: сообщение об успешной регистрации
else
System --> User: сообщение о необходимости скорректировать и повторно ввести данные
end alt
@enduml

DDD, ООП и UML для аналитика

Код курса
BUML
Ближайшая дата курса
9 декабря, 2024
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

В Python-коде Flask-приложения я реализовала это так:

# Установка необходимых библиотек
!pip install flask
!pip install pyngrok
!pip install flask-ngrok
# Импорт модулей и библиотек
import psycopg2
import traceback
import threading
from flask_ngrok import run_with_ngrok
import os

# Строка подключения к БД
connection_string='postgres://my-instance'
conn = psycopg2.connect(connection_string)
# Установка токена для авторизации в ngrok #@param {type:"string"}
auth_token = "my-ngrok-token"
os.system(f"ngrok authtoken {auth_token}")

# Функция регистрации пользователя (добавления записи в таблицу users)
def add_user_to_db(conn, user_data):
    cursor = conn.cursor()
    cursor.execute("SELECT COUNT(id) FROM users")
    id = cursor.fetchone()[0] + 1
    try:
        cursor.execute("INSERT INTO users (id, login, password, role) VALUES (%s, %s, %s, %s)", (id, user_data['login'], user_data['password'], user_data['role']))
        conn.commit()
        user = {"id": id, "login": user_data['login'], "password": user_data['password'], "role": user_data['role']}
        return user
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()
# Создание экземпляра Flask-приложения
app = Flask(__name__)
# Запуск приложения с использованием ngrok
run_with_ngrok(app)

# Обработчик регистрации нового пользователя:
@app.route('/registration', methods=['POST', 'GET'])
def registration():
    conn = psycopg2.connect(connection_string)
    try:
        if request.method == 'POST':
            if request.content_type == 'application/json':
                user_data = request.json
            elif request.form:
                user_data = request.form
            login = user_data.get('login')
            password = user_data.get('password')
            role = user_data.get('role')

            # Регистрация пользователя в базе данных
            added_user = add_user_to_db(conn, user_data)
            response = make_response(render_template('login.html'), 200)
            return response
        else:
            return render_template('registration.html')
    except Exception as e:
        traceback.print_exc()
        if request.method == 'POST':
            return jsonify({"error": f'Ошибка при добавлении пользователя в базу данных: {e}'})
        else:
            return jsonify({"error": f'Сервис регистрации недоступен, попробуйте позже: {e}'})
    finally:
        if conn is not None:
            conn.close()

Разумеется, в реальных системах пароли не хранятся в БД в открытом виде, а шифруются или хэшируются. Однако, поскольку сейчас я реализую демо-пример для обучения, безопасность уступила наглядности).

Основы архитектуры и интеграции информационных систем

Код курса
OAIS
Ближайшая дата курса
20 января, 2025
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

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

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

В моем REST-приложении аутентификация нужна для того, позволить выполнять операции изменения данных о товарах, поставщиках и заказах только пользователям с ролью Менеджер. Поскольку пользователь уже зарегистрирован в системе, т.е. запись с его логином, паролем и ролью есть в таблице users, аутентификация представляет собой сопоставление данных, введенных пользователем на странице входа в систему, с данными, которые есть в базе. Помимо этой проверки подлинности нахождения пользователя в базе, процедура аутентификации в моем приложении включает генерацию JWT-токена, который будет давать право на выполнение отдельных вариантов использования. Однако, в этот раз вместо авторизации на основе токена я решила использовать куки-файлы (cookie), поскольку это проще и не требует явного сохранения JWT. Работает это так: серверное приложение отправляет клиенту (браузеру) файл куки, который автоматически вставляется в заголовок каждого последующего HTTP-запроса. Срок жизни куки ограничен временем клиентской сессии. Для повышения надежности я решила формировать значение куки на основе сгенерированного JWT-токена с id пользователя в БД и его ролью, а также временем выдачи токена.

На UML-диаграмме последовательности это выглядит следующим образом:

UML, системный анализ, куки-аутентификация пользователя диаграмма последовательности пример
UML-диаграмма последовательности куки-аутентификации в веб-приложении

PlantUML-скрипт:

@startuml
title: Аутентификация пользователя (вход в систему)
actor User
participant Серверное_Приложение as System
database БД
User -> System: войти() через GET-запрос к маршруту /login
System --> User: страница входа в систему
User -> System: отправить данные для аутентификации(login, password, role) через POST-запрос к маршруту /login
System -> System: Проверить валидность данных()
alt данные валидны
System -> БД: найти пользователя(login, password, role)
БД -> БД: SELECT * FROM users WHERE login = login AND password = password AND role=role
alt пользователь найден
БД --> System: данные пользователя(id, login, password, role)
alt role='manager'
System -> System: сгенерировать JWT-токен для пользователя (user_id, role, exp_time)
System -> БД: узнать количество строк в таблице jwts для вычисления id()
БД -> БД: SELECT COUNT(id) FROM jwts
БД --> System: количество строк в таблице jwts
System -> System: сгенерировать id для новой записи(количество строк в таблице jwts + 1)
System -> System: создать объект для записи в БД(id, exp_time, token, user_id)
System -> БД: создать нового JWT-токена(id, exp_time, token, user_id) через INSERT-запрос к таблице jwts
БД -> БД: INSERT INTO jwts (id, published, token, sysuser) VALUES (id, JWTtime, token, sysuser)
БД --> System: запись создана
System -> System: добавить токен в cookie через set_cookie('token', token)
else пользователь - покупатель
System -> System: отобразить главную страницу
end alt
System --> User: запрос на аутентификацию выполнен, статус HTTP-ответа 200
else
БД --> System: None
System --> User: сообщение о необходимости зарегистрироваться
end alt
else данные не валидны
System --> User: сообщение о необходимости исправить данные и повторить ввод
end alt
@enduml

В моем Flask-приложении это реализовано следующим программным кодом:

# Импорт модулей и библиотек
import jwt
from flask import Flask, session, jsonify, request, render_template, response, make_response
from datetime import date, datetime, timedelta
# Функция поиска пользователя в базе данных
def find_user_in_db(conn, login, password, role):
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE login = %s AND password = %s AND role=%s", (login, password, role))
    result = cursor.fetchone()
    if result:
        user = {"id": result[0], "login": result[1], "password": result[2], "role": result[3]}
        return user
    else:
        return None

# Функция генерации JWT-токена

def generate_jwt_token(user_id, role, expiration_minutes):
  if role=='manager':
    # Задаем время жизни токена
    expiration_time = datetime.utcnow() + timedelta(minutes=expiration_minutes)

    # Создаем словарь с данными пользователя, которые будут включены в токен
    payload = {
        "user_id": user_id,
         "role": role,
        "exp": expiration_time
    }

    # Генерируем JWT токен с помощью секретного ключа
    token = jwt.encode(payload, secret_key, algorithm="HS256")

    return token

# Функция записи JWT-токена в таблицу jwts
def add_JWT_to_db(conn, JWTtime, token, sysuser):
    cursor = conn.cursor()
    cursor.execute("SELECT COUNT(id) FROM jwts")
    id = cursor.fetchone()[0] + 1
    try:
        cursor.execute("INSERT INTO jwts (id, published, token, sysuser) VALUES (%s, %s, %s, %s)", (id, JWTtime, token, sysuser))
        conn.commit()
        return token
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()

# Обработчик входа в систему существующего пользователя:
@app.route('/login', methods=['POST', 'GET'])
def login():
    conn = psycopg2.connect(connection_string)
    try:
        if request.method == 'POST':
            if request.content_type == 'application/json':
                user_data = request.json
            elif request.form:
                user_data = request.form
            login = user_data.get('login')
            password = user_data.get('password')
            role = user_data.get('role')

            # Поиск пользователя в базе данных
            user = find_user_in_db(conn, login, password, role)
            if user is not None:
                # Генерация JWT-токена
              if role =='manager':
                token = generate_jwt_token(user, role, exp_time)
                sysuser=user['id']
                print('token', token)
                # Запись JWT-токена в таблицу jwts
                add_JWT_to_db(conn, datetime.utcnow(), token, sysuser)
                # Добавление токена в заголовок запроса
                response = make_response(render_template('index.html'), 200)
                response.set_cookie('token', token)
                return response
              else:
               return render_template('index.html')
            else:
                response = make_response(render_template('registration.html'), 401)
                return response
        else:
            return render_template('login.html')
    except Exception as e:
        traceback.print_exc()
        if request.method == 'POST':
            return jsonify({"error": f'Ошибка аутентификации пользователя в базу данных: {e}'})
        else:
            return jsonify({"error": f'Сервис аутентификации недоступен, попробуйте позже: {e}'})
    finally:
        if conn is not None:
            conn.close()

Справедливости ради стоит отметить, что простая аутентификация на основе куки не считается безопасной. Cookie уязвимы для атак межсайтового скриптинга (XSS, Cross-Site Scripting) и подделки межсайтовых запросов (CSRF, Сross Site Request Forgery). Впрочем, разработчики умеют обходить эти риски с помощью специальных приемов. Также куки нельзя назвать универсальным способом аутентификации: они отлично подходят для браузеров, но не для мобильных приложений.

Основы архитектуры и интеграции информационных систем

Код курса
OAIS
Ближайшая дата курса
20 января, 2025
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

Авторизация на примере UC «Посмотреть список заказов»

Наконец, рассмотрим, как cookie-аутентификация используется для авторизации, т.е. проверки прав на выполнение пользователем определенных операций с данными. Согласно проекту REST API и спецификации OpenAPI, описанным в предыдущей статье, некоторые варианты использования доступны только сотрудникам интернет-магазина. Например, просмотреть все заказы может только зарегистрированный пользователь с ролью Менеджер. Поскольку у меня реализована аутентификация на основе куки, в рамках авторизации будет проверяться валидность куки-файла в запросе к определенной конечной точке.

На UML-диаграмме последовательности это будет выглядеть так:

UML, системный анализ, куки-аутентификация пользователя диаграмма последовательности авторизация веб-приложения REST API пример
UML-диаграмма последовательности авторизации пользователя в REST API

Скрипт Plantuml:

@startuml
title: Просмотр списка заказов
actor User
participant Серверное_Приложение as System
database БД

User -> System: посмотреть заказы() через GET-запрос к маршруту /order
System -> System: получить значение для ключа token в куки-запроса(GET-запрос)

alt есть куки
    System -> System: извлечь данные пользователя из куки-токена (token)
    System -> БД: найти токен(sysuser)
    БД -> БД: SELECT token FROM jwts WHERE sysuser = sysuser
    
    alt токен найден
        БД --> System: токен
        System -> System: проверить срок жизни токена(token)
        
        alt токен валиден
            System -> System: расшифровать токен(token)
            System -> System: проверить роль носителя токена(token)
            
            alt role='manager'
                System -> БД: получить список заказов() 
                БД -> БД: SELECT orders.id, customer.name, order_states.name, delivery.address, orders.sum, orders.date FROM orders JOIN customer on customer.id=orders.customer JOIN order_states on order_states.id=orders.state JOIN delivery on delivery.id=orders.delivery ORDER BY orders.id DESC
                БД --> System: результаты SQL-запроса
                System --> User: список заказов
            else другая роль пользователя
                System --> User: сообщение о необходимости войти в систему как менеджер, статус 403
            end alt
        else время жизни токена истекло
            System --> User: сообщение о необходимости войти в систему, статус 401
        end alt
    else
        System --> User: сообщение о необходимости войти в систему, статус 401
    end alt
        System --> User: сообщение о необходимости войти в систему, статус 401
end alt
@enduml

Реализация в коде:

# Задаем секретный ключ для подписи JWT
secret_key = "SecretKey4JWT"
# Задаем время жизни JWT-токена в минутах
exp_time=15

# Функция поиска токена в базе данных
def find_token_in_db(conn, sysuser):
  try:
    cursor = conn.cursor()
    print('sysuser', sysuser)
    cursor.execute("SELECT token FROM jwts WHERE sysuser = %s::integer", (sysuser,))
    token = cursor.fetchone()
    return token
  except Exception as e:
    traceback.print_exc()
    return None

# Функция извлечения данных пользователя из токена
def extract_user_data_from_token(token):
    try:
        # Декодируем токен для получения данных пользователя
        decoded_token = jwt.decode(token, secret_key, algorithms=["HS256"])
        user_id = decoded_token["user_id"]
        sysuser = user_id["id"]
        if find_token_in_db(conn, sysuser):
          return decoded_token
    except jwt.exceptions.ExpiredSignatureError:
        print("Время жизни токена истекло, надо снова войти в систему")
        response = make_response(render_template('login.html'), 401)
        return response
    except jwt.exceptions.DecodeError:
        return None

# Обработчик входа в систему существующего пользователя:
@app.route('/login', methods=['POST', 'GET'])
def login():
    conn = psycopg2.connect(connection_string)
    try:
        if request.method == 'POST':
            if request.content_type == 'application/json':
                user_data = request.json
            elif request.form:
                user_data = request.form
            login = user_data.get('login')
            password = user_data.get('password')
            role = user_data.get('role')

            # Поиск пользователя в базе данных
            user = find_user_in_db(conn, login, password, role)
            if user is not None:
                # Генерация JWT-токена
              if role =='manager':
                token = generate_jwt_token(user, role, exp_time)
                sysuser=user['id']
                print('token', token)
                # Запись JWT-токена в таблицу jwts
                add_JWT_to_db(conn, datetime.utcnow(), token, sysuser)
                # Добавление токена в заголовок запроса
                response = make_response(render_template('index.html'), 200)
                response.set_cookie('token', token)
                return response
              else:
               return render_template('index.html')
            else:
                response = make_response(render_template('registration.html'), 401)
                return response
        else:
            return render_template('login.html')
    except Exception as e:
        traceback.print_exc()
        if request.method == 'POST':
            return jsonify({"error": f'Ошибка аутентификации пользователя в базу данных: {e}'})
        else:
            return jsonify({"error": f'Сервис аутентификации недоступен, попробуйте позже: {e}'})
    finally:
        if conn is not None:
            conn.close()

Разработка ТЗ на информационную систему по ГОСТ и SRS

Код курса
TTIS
Ближайшая дата курса
2 декабря, 2024
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

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

 

Я даю свое согласие на обработку персональных данных и соглашаюсь с политикой конфиденциальности.

Добавить комментарий