Как защитить REST API Flask приложение с помощью JSON Web Token?
Мы создадим службу базы данных, используя SQLite, и позволим пользователям получать к ней доступ через API REST, используя методы HTTP, такие как POST и PUT.
Кроме того, мы узнаем, почему JSON Web Tokens являются подходящим способом защиты остальных API вместо базовой аутентификации. Прежде чем мы продолжим, давайте разберемся с терминами веб-токенов JSON, REST API и инфраструктурой Flask.
JSON Web Tokens
JSON Web Tokens, также известный как JWT, является безопасным способом передачи случайных токенов между двумя сторонами или объектами. JSON обычно состоит из трех частей:
- Заголовок (Header)
- Загрузка (Payload)
- Подпись (Signature)
JSON использует два типа структурных форм при передаче данных или информации между двумя сторонами.
- Сериализированные
- Десериализованный
Сериализованная форма используется при передаче данных в сеть через каждый запрос и ответ, тогда как десериализованная форма используется при чтении и записи данных в веб-токен.
В сериализованной форме есть три компонента.
- Заголовок (Header)
- Загрузка (Payload)
- Подпись (Signature)
Компонент заголовка определяет криптографическую информацию о токенах. Например:
- Это подписанный или неподписанный JWT?
- Определить методы алгоритма
Десериализованная форма, в отличие от сериализованной формы, содержит два компонента.
- Заголовок (Header)
- Загрузка (Payload)
REST API
API (интерфейс прикладного программирования) позволяет взаимодействовать между двумя приложениями для получения или отправки данных. Существует два популярных типа API - веб API и системный API.
В этой статье мы рассмотрим только веб API. Существует два типа веб-API.
- Запрос - API ответа: Rest, GraphQL, Удаленный вызов процедур (RPC)
- Управляемый событиями API: WebHooks, веб-сокеты, потоковая передача HTTP
REST API подпадает под категорию запрос-ответ. Он использует методы HTTP, такие как GET, POST и PUT, для выполнения операций API.
Классический пример - когда пользователь отправляет метод GET веб-службе для запроса или извлечения определенного ресурса или коллекции ресурсов. Затем сервер отправляет обратно определенный ресурс или набор ресурсов обратно пользователю, который его запросил.
Flask Framework
Flask - это фреймворк, основанный на python. Это микро-фреймворк, используемый разработчиками Python для создания API остальных. Он называется микро-фреймворком, потому что он позволяет разработчикам, например, добавлять пользовательскую аутентификацию и любую другую серверную систему на основе предпочтений.
Давайте начнем с реализации. Моя система настройки выглядит следующим образом.
- Ubuntu
- Python 2.7
- Postman
Настройте виртуальную среду с помощью virtualenv
Нам нужно настроить виртуальную среду, чтобы некоторые пакеты не конфликтовали с системными пакетами. Давайте использовать virtualenv
для настройки новой виртуальной среды.
Предполагая, что у вас есть команда pip
, доступная в вашей системе, выполните следующую команду через, pip
чтобы установить virtualenv
.
pip install virtualenv
Если у вас нет pip на вашем компьютере, следуйте этой документации, чтобы установить pip в вашей системе.
Далее, давайте создадим каталог для хранения нашей виртуальной среды. Используйте команду mkdir
, показанную ниже, чтобы создать каталог
mkdir flaskproject
Перейдите в каталог flaskproject
с помощью следующей команды
cd flaskproject
Внутри каталога flaskproject
используйте инструмент virtualenv
для создания виртуальной среды, как показано ниже:
virtualenv flaskapi
После того, как вы воспользовались инструментом virtualenv
для создания виртуальной среды, выполните команду cd
, чтобы перейти в каталог flaskapi
в качестве виртуальной среды, и активируйте его, используя приведенную команду ниже.
source bin/activate
Выполните все задачи, связанные с этим проектом, в виртуальной среде.
Установка пакетов с помощью pip
Теперь пришло время установить такие пакеты, как фреймворк Flask и PyJWT, которые мы будем использовать для создания остальных API и других необходимых пакетов для нашего проекта API.
Создайте файл requirements.txt
со следующими пакетами.
Flask
datetime
uuid
Flask-SQLAlchemy
PyJWT
Установите их с помощью pip.
pip install -r requirements.txt
Настройте базу данных
Давайте установим SQLite
apt-get install sqlite3
Создаем базу данных под названием library. Внутри этой базы данных мы создадим две таблицы, а именно Users и Authors.
Таблица пользователей будет содержать зарегистрированных пользователей. Только зарегистрированные пользователи могут иметь доступ к таблице авторов.
Таблица авторов будет хранить информацию об авторах или данные, такие как имя автора, страна рождения и т.д.
Создайте базу данных, используя следующую команду:
sqlite3 library.db
Вы можете проверить, успешно ли вы создали базу данных, используя следующую команду:
.databases
Откройте новый терминал и выполните следующее в виртуальной среде, которую мы создали ранее.
touch app.py
Вставьте следующий код в файл с именем app.py
from flask import Flask, request, jsonify, make_response
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import uuid
import jwt
import datetime
from functools import wraps
Первая строка в приведенном выше коде импортирует пакеты, такие как request
и jsonify
. Мы будем использовать request
для отслеживания данных запроса и использовать jsonify
для вывода ответов в формате JSON.
На следующей строке мы импортировали SQLAlchemy
из flask_sqlalchemy, чтобы интегрировать функции SQLAlchemy в колбу.
Из werkzeug.security
, мы импортировали, generate_password_hash
чтобы сгенерировать хэш пароля для пользователей и check_password_hash
проверить пароль пользователя, хранящийся в базе данных.
Наконец, мы импортировали uuid
также известные как универсальные уникальные идентификаторы для генерации случайных чисел для пользователей.
Внутри файла app.py
реализуйте параметры конфигурации для API библиотеки, используя код ниже.
Поместите следующий код под строкой импорта.
app = Flask(__name__)
app.config['SECRET_KEY']='Th1s1ss3cr3t'
app.config['SQLALCHEMY_DATABASE_URI']='sqlite://///home/michael/geekdemos/geekapp/library.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
Теперь создайте две модели для таблицы Users и Авторы, как показано ниже. Скопируйте и вставьте код в файл app.py.
Поместите код ниже прямо под этой настройкой базы данных db = SQLAlchemy(app)
class Users(db.Model):
id = db.Column(db.Integer, primary_key=True)
public_id = db.Column(db.Integer)
name = db.Column(db.String(50))
password = db.Column(db.String(50))
admin = db.Column(db.Boolean)
class Authors(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False))
book = db.Column(db.String(20), unique=True, nullable=False))
country = db.Column(db.String(50), nullable=False))
booker_prize = db.Column(db.Boolean)
Генерация таблиц пользователей и авторов
В терминале введите следующий код в виртуальной среде, чтобы сгенерировать или создать таблицы для таблиц Users и Authors, как показано ниже.
from app import db
db.create_all()
После этого откройте файл app.py
в виртуальной среде и создайте другую функцию.
Эта функция генерирует токены, чтобы разрешить только зарегистрированным пользователям получать доступ и выполнять набор операций API с таблицей авторов.
Поместите этот код под модель базы данных для таблицы авторов
def token_required(f):
@wraps(f)
def decorator(*args, **kwargs):
token = None
if 'x-access-tokens' in request.headers:
token = request.headers['x-access-tokens']
if not token:
return jsonify({'message': 'a valid token is missing'})
try:
data = jwt.decode(token, app.config[SECRET_KEY])
current_user = Users.query.filter_by(public_id=data['public_id']).first()
except:
return jsonify({'message': 'token is invalid'})
return f(current_user, *args, **kwargs)
return decorator
Создание маршрутов для таблицы пользователей
Теперь давайте создадим маршрут, позволяющий пользователям регистрироваться в API авторов через имя пользователя и пароль, как показано ниже.
Снова откройте файл app.py
в виртуальной среде и вставьте следующий код под функцию token_required(f)
@app.route('/register', methods=['GET', 'POST'])
def signup_user():
data = request.get_json()
hashed_password = generate_password_hash(data['password'], method='sha256')
new_user = Users(public_id=str(uuid.uuid4()), name=data['name'], password=hashed_password, admin=False)
db.session.add(new_user)
db.session.commit()
return jsonify({'message': 'registered successfully'})
Внутри виртуальной среды создайте другой маршрут в файле app.py
, чтобы разрешить зарегистрированным пользователям входить в систему.
Когда пользователь входит в систему, генерируется случайный токен для доступа пользователя к API библиотеки.
Вставьте приведенный ниже код под предыдущий маршрут, который мы создали.
@app.route('/login', methods=['GET', 'POST'])
def login_user():
auth = request.authorization
if not auth or not auth.username or not auth.password:
return make_response('could not verify', 401, {'WWW.Authentication': 'Basic realm: "login required"'})
user = Users.query.filter_by(name=auth.username).first()
if check_password_hash(user.password, auth.password):
token = jwt.encode({'public_id': user.public_id, 'exp' : datetime.datetime.utcnow() + datetime.timedelta(minutes=30)}, app.config['SECRET_KEY'])
return jsonify({'token' : token.decode('UTF-8')})
return make_response('could not verify', 401, {'WWW.Authentication': 'Basic realm: "login required"'})
Затем в виртуальной среде создайте другой маршрут в файле app.py
для получения всех зарегистрированных пользователей.
Этот код проверяет всех зарегистрированных пользователей в таблице Users и возвращает окончательный результат в формате JSON.
Вставьте код ниже под маршрутом входа
@app.route('/users', methods=['GET'])
def get_all_users():
users = Users.query.all()
result = []
for user in users:
user_data = {}
user_data['public_id'] = user.public_id
user_data['name'] = user.name
user_data['password'] = user.password
user_data['admin'] = user.admin
result.append(user_data)
return jsonify({'users': result})
Создание маршрутов для таблицы авторов
Давайте создадим маршруты для таблицы Авторы, чтобы пользователи могли получать всех авторов в базе данных, а также удалять авторов.
Только пользователи с действительными токенами могут выполнять эти операции API.
Внутри файла app.py
создайте маршрут для зарегистрированных пользователей для создания новых авторов.
Вставьте этот код под маршрут, который позволяет пользователю получить всех зарегистрированных пользователей.
@app.route('/author', methods=['POST', 'GET'])
@token_required
def create_author(current_user):
data = request.get_json()
new_authors = Authors(name=data['name'], country=data['country'], book=data['book'], booker_prize=True, user_id=current_user.id)
db.session.add(new_authors)
db.session.commit()
return jsonify({'message' : 'new author created'})
Затем создайте другой маршрут, чтобы позволить зарегистрированному пользователю с действительным токеном получить всех авторов в таблице авторов, как показано ниже.
Вставьте этот код ниже маршрута, который позволяет пользователю создать нового автора.
@app.route('/authors', methods=['POST', 'GET'])
@token_required
def get_authors(current_user):
authors = Authors.query.filter_by(user_id=current_user.id).all()
output = []
for author in authors:
author_data = {}
author_data['name'] = author.name
author_data['book'] = author.book
author_data['country'] = author.country
author_data['booker_prize'] = author.booker_prize
output.append(author_data)
return jsonify({'list_of_authors' : output})
Наконец, по-прежнему внутри файла app.py
создайте маршрут для удаления указанного автора, как показано ниже.
Вставьте этот код под маршрут, который позволяет пользователю получить список авторов.
@app.route('/authors/<author_id>', methods=['DELETE'])
@token_required
def delete_author(current_user, author_id):
author = Author.query.filter_by(id=author_id, user_id=current_user.id).first()
if not author:
return jsonify({'message': 'author does not exist'})
db.session.delete(author)
db.session.commit()
return jsonify({'message': 'Author deleted'})
if __name__ == '__main__':
app.run(debug=True)
После этого сохраните и закройте файл app.py в виртуальной среде.
Полный код файла app.py
, должен выглядеть так:
from flask import Flask, request, jsonify, make_response
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import uuid
import jwt
import datetime
from functools import wraps
app = Flask(__name__)
app.config['SECRET_KEY']='Th1s1ss3cr3t'
app.config['SQLALCHEMY_DATABASE_URI']='sqlite://///home/michael/geekdemos/geekapp/library.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
class Users(db.Model):
id = db.Column(db.Integer, primary_key=True)
public_id = db.Column(db.Integer)
name = db.Column(db.String(50))
password = db.Column(db.String(50))
admin = db.Column(db.Boolean)
class Authors(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
book = db.Column(db.String(20), unique=True, nullable=False)
country = db.Column(db.String(50), nullable=False)
booker_prize = db.Column(db.Boolean)
user_id = db.Column(db.Integer)
def token_required(f):
@wraps(f)
def decorator(*args, **kwargs):
token = None
if 'x-access-tokens' in request.headers:
token = request.headers['x-access-tokens']
if not token:
return jsonify({'message': 'a valid token is missing'})
try:
data = jwt.decode(token, app.config[SECRET_KEY])
current_user = Users.query.filter_by(public_id=data['public_id']).first()
except:
return jsonify({'message': 'token is invalid'})
return f(current_user, *args, **kwargs)
return decorator
@app.route('/register', methods=['GET', 'POST'])
def signup_user():
data = request.get_json()
hashed_password = generate_password_hash(data['password'], method='sha256')
new_user = Users(public_id=str(uuid.uuid4()), name=data['name'], password=hashed_password, admin=False)
db.session.add(new_user)
db.session.commit()
return jsonify({'message': 'registered successfully'})
@app.route('/login', methods=['GET', 'POST'])
def login_user():
auth = request.authorization
if not auth or not auth.username or not auth.password:
return make_response('could not verify', 401, {'WWW.Authentication': 'Basic realm: "login required"'})
user = Users.query.filter_by(name=auth.username).first()
if check_password_hash(user.password, auth.password):
token = jwt.encode({'public_id': user.public_id, 'exp' : datetime.datetime.utcnow() + datetime.timedelta(minutes=30)}, app.config['SECRET_KEY'])
return jsonify({'token' : token.decode('UTF-8')})
return make_response('could not verify', 401, {'WWW.Authentication': 'Basic realm: "login required"'})
@app.route('/user', methods=['GET'])
def get_all_users():
users = Users.query.all()
result = []
for user in users:
user_data = {}
user_data['public_id'] = user.public_id
user_data['name'] = user.name
user_data['password'] = user.password
user_data['admin'] = user.admin
result.append(user_data)
return jsonify({'users': result})
@app.route('/authors', methods=['GET', 'POST'])
@token_required
def get_authors(current_user, public_id):
authors = Authors.query.all()
output = []
for author in authors:
author_data = {}
author_data['name'] = author.name
author_data['book'] = author.book
author_data['country'] = author.country
author_data['booker_prize'] = author.booker_prize
output.append(author_data)
return jsonify({'list_of_authors' : output})
@app.route('/authors', methods=['POST', 'GET'])
@token_required
def create_author(current_user):
data = request.get_json()
new_authors = Authors(name=data['name'], country=data['country'], book=data['book'], booker_prize=True, user_id=current_user.id)
db.session.add(new_authors)
db.session.commit()
return jsonify({'message' : 'new author created'})
@app.route('/authors/<name>', methods=['DELETE'])
@token_required
def delete_author(current_user, name):
author = Author.query.filter_by(name=name, user_id=current_user.id).first()
if not author:
return jsonify({'message': 'author does not exist'})
db.session.delete(author)
db.session.commit()
return jsonify({'message': 'Author deleted'})
if __name__ == '__main__':
app.run(debug=True)
Тестирование API библиотеки с Postman
В этом разделе мы будем использовать инструмент postman для отправки запроса в службы базы данных. Если на вашем компьютере нет postman, вы можете загрузить и установить его здесь.
Помимо Postman, мы можем использовать другие инструменты, такие как Curl, для отправки запросов на сервер.
Откройте новый терминал и введите следующее:
postman
Команда postman
заставит ваш веб-браузер отобразить страницу ниже:
Вы можете зарегистрироваться и создать бесплатную учетную запись, но мы пропустим и получим прямой доступ к приложению для тестирования API библиотеки, как показано ниже:
В этом разделе мы позволим пользователю зарегистрироваться для API библиотеки, предоставив имя пользователя и уникальный пароль в формате JSON с помощью метода POST, выполнив следующие шаги:
- Нажмите на вкладку с надписью Body
- Затем нажмите кнопку Raw и выберите формат JSON.
- Введите имя пользователя и пароль для регистрации, как показано на скриншоте
- Рядом с кнопкой отправки вставьте следующий URL-адрес http://127.0.0.1/register
- Наконец, измените метод на POST и нажмите кнопку отправки.
Он отобразит следующий вывод, как показано ниже:
Теперь мы успешно зарегистрировали пользователя. Давайте продолжим, чтобы позволить только что зарегистрированному пользователю войти в систему, чтобы сгенерировать временный случайный токен, для доступа к таблице авторов, выполнив следующие шаги:
- Нажмите на вкладку авторизации.
- Под разделом TYPE выберите Base Auth.
- Затем заполните форму с именем пользователя и паролем, которые вы зарегистрировали ранее.
- Наконец, нажмите кнопку отправки, чтобы войти в систему и сгенерировать случайный токен.
После успешного входа пользователя генерируется случайный токен, как показано на скриншоте.
Мы будем использовать сгенерированный случайный токен для доступа к таблице Authors.
В этом разделе мы добавим информацию об авторе в таблицу Authors с помощью метода POST, выполнив следующие шаги:
- Нажмите на вкладку заголовков
- Включите следующие заголовки HTTP, показанные на скриншоте
- Далее нажмите на вкладку Body и введите данные нового автора
- Затем нажмите кнопку отправки, чтобы добавить сведения об авторе в таблицу авторов.
Вы также можете получить информацию об авторах в таблице авторов с помощью следующего:
- Убедитесь, что ваш сгенерированный токен находится в разделе заголовков. если его там нет, вам нужно заполнить его токеном.
- Рядом с кнопкой отправки введите этот URL http://127.0.0.1/authors
- Затем измените метод HTTP на GET и нажмите кнопку отправки, чтобы получить сведения об авторах.
Наконец, вы можете удалить автора (ов) в таблице Authors с помощью метода DELETE, выполнив следующие шаги:
- Убедитесь, что ваш токен все еще находится в разделе заголовков. Вы можете проверить вкладку заголовков, чтобы убедиться в наличии необходимой информации.
- Рядом с кнопкой отправки введите этот URL http://127.0.0.1/sam
- Затем нажмите кнопку отправки, чтобы удалить указанного вами пользователя.
Полный исходный код проекта, можете найти на Github. Вы можете скачать его и проверить на своей машине.