Недавно я рассказывала про аутентификацию в веб-приложениях с помощью JWT-токена. Сегодня рассмотрим практическую реализацию регистрации пользователей и аутентификацию клиентов на сервере с помощью куки-файлов. Как обычно, в качестве примера возьмем интернет-магазин, представляющий собой серверное Flask-приложение, запущенное в Google Colab и тунеллированное с помощью утилиты ngrok.
Регистрация пользователя: проектирование и реализация
На основе проектирования модели данных и REST API серверного приложения, выполненного в прошлой статье, рассмотрим, как именно реализовать ролевое разделение вариантов использования между акторами системы. Например, в области управления товарами вариант использования UC 1.1 Посмотреть список товаров доступен для всех пользователей, добавление нового товара, изменение или удаление существующего возможно только для Менеджеров магазина. Необходимость аутентификации как системного варианта использования показывается на UML-диаграмме Use Case с помощью связи include. Например, пользователь с ролью Менеджер может добавить товар, что включает его аутентификацию, т.е. проверку того, что этот пользователь действительно является менеджером.
Напомню, процедуре аутентификации пользователя предшествует его регистрация. Для этого в модели данных проектируемой системы реализована таблица users. Диаграмма последовательности выглядит так:
Скрипт 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-диаграмме последовательности это выглядит следующим образом:
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-диаграмме последовательности это будет выглядеть так:
Скрипт 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 руб.
Полный код реализованного приложения и демонстрацию его работы я покажу в следующей статье, а пока напомню, что познакомиться со всеми использованными в этом материале и другими техниками работы аналитика вам помогут мои курсы в Школе прикладного бизнес-анализа на базе нашего лицензированного учебного центра обучения и повышения квалификации системных и бизнес-аналитиков в Москве:
- UML для бизнес-аналитика
- Основы архитектуры и интеграции информационных систем
- Разработка ТЗ на информационную систему