Как защитить 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 остальных. Он называется микро-фреймворком, потому что он позволяет разработчикам, например, добавлять пользовательскую аутентификацию и любую другую серверную систему на основе предпочтений.

Давайте начнем с реализации. Моя система настройки выглядит следующим образом.

Настройте виртуальную среду с помощью 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 заставит ваш веб-браузер отобразить страницу ниже:

Postman

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

Postman

В этом разделе мы позволим пользователю зарегистрироваться для API библиотеки, предоставив имя пользователя и уникальный пароль в формате JSON с помощью метода POST, выполнив следующие шаги:

  • Нажмите на вкладку с надписью Body
  • Затем нажмите кнопку Raw и выберите формат JSON.
  • Введите имя пользователя и пароль для регистрации, как показано на скриншоте
  • Рядом с кнопкой отправки вставьте следующий URL-адрес http://127.0.0.1/register
  • Наконец, измените метод на POST и нажмите кнопку отправки.
Postman

Он отобразит следующий вывод, как показано ниже:

Postman

Теперь мы успешно зарегистрировали пользователя. Давайте продолжим, чтобы позволить только что зарегистрированному пользователю войти в систему, чтобы сгенерировать временный случайный токен, для доступа к таблице авторов, выполнив следующие шаги:

  •  Нажмите на вкладку авторизации.
  • Под разделом TYPE выберите Base Auth.
  • Затем заполните форму с именем пользователя и паролем, которые вы зарегистрировали ранее.
  • Наконец, нажмите кнопку отправки, чтобы войти в систему и сгенерировать случайный токен.
Postman

После успешного входа пользователя генерируется случайный токен, как показано на скриншоте.

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

Postman

В этом разделе мы добавим информацию об авторе в таблицу Authors с помощью метода POST, выполнив следующие шаги:

  • Нажмите на вкладку заголовков
  • Включите следующие заголовки HTTP, показанные на скриншоте
Postman
  • Далее нажмите на вкладку Body и введите данные нового автора
  • Затем нажмите кнопку отправки, чтобы добавить сведения об авторе в таблицу авторов.
Postman

Вы также можете получить информацию об авторах в таблице авторов с помощью следующего:

  • Убедитесь, что ваш сгенерированный токен находится в разделе заголовков. если его там нет, вам нужно заполнить его токеном.
  • Рядом с кнопкой отправки введите этот URL http://127.0.0.1/authors
  • Затем измените метод HTTP на GET и нажмите кнопку отправки, чтобы получить сведения об авторах.
Postman

Наконец, вы можете удалить автора (ов) в таблице Authors с помощью метода DELETE, выполнив следующие шаги:

  • Убедитесь, что ваш токен все еще находится в разделе заголовков. Вы можете проверить вкладку заголовков, чтобы убедиться в наличии необходимой информации.
  • Рядом с кнопкой отправки введите этот URL http://127.0.0.1/sam
  • Затем нажмите кнопку отправки, чтобы удалить указанного вами пользователя.
Postman

Полный исходный код проекта, можете найти на Github. Вы можете скачать его и проверить на своей машине.