Русский
Русский
English
Статистика
Реклама

Rest api

Best practices для клиент-серверного проекта PoC

22.12.2020 12:11:10 | Автор: admin
image
Типичный проект PoC (Proof of Concept) состоит из клиента с GUI, сервера c бизнес логикой и API между ними. Также используется база данных, хранящая оперативную информацию и данные пользователей. Во многих случаях необходима связь с внешними системами со своим API.
Когда у меня возникла необходимость в создании проекта PoC, и я начал разбираться в деталях, то оказалось, что порог вхождения в веб-программирование весьма высок. В крупных проектах для каждого компонента есть выделенные специалисты: front-end, back-end разработчики, UX/UI дизайнеры, архитекторы баз данных, специалисты по API и информационной безопасности, системные администраторы. В небольшом PoC надо самому во всем разобраться, выбрать подходящее техническое решение, реализовать и развернуть. Ситуацию ухудшает тот факт, что из обучающих материалов не всегда понятно, почему предлагается сделать именно так, а не иначе, есть ли альтернативы, является ли решение best practice или это частное мнение автора. Поэтому я разработал заготовку под названием Common Test DB, отвечающую лучшим практикам. Ее можно использовать для начала любого проекта, остается только наполнить функциональным смыслом.
В статье я подробно опишу примененные best practices, расскажу про имеющиеся альтернативы и в конце размещу ссылки на исходники и работающий в сети пример.

Требования к проекту PoC


Начнем с минимальных требований к проекту PoC. В статье Блокчейн: что нам стоит PoC построить? я уже рассматривал из каких частей состоит универсальная информационная система. В этой статье подробно разберем состав типичного проекта PoC. Даже минимальный проект в клиент-серверной архитектуре требует значительной функциональности и технической проработки:

Клиент:
  • Должен осуществлять запросы к серверу, используя REST API
  • Регистрировать, авторизовать, аутентифицировать пользователей
  • Принимать, разбирать и визуализировать пришедшие данные

Сервер
  • Должен принимать HTTP/HTTPS запросы от клиента
  • Осуществлять взаимодействие с внешними системами
  • Взаимодействовать с базой данных (реализовать базовую функциональности CRUD)
  • Обеспечивать безопасность данных, вызовов API и конфигурационных параметров
  • Осуществлять логирование и мониторинг работоспособности

База данных
  • Необходимо реализовать структуру DB
  • Наполнить DB начальными значениями

Архитектура
  • Проект PoC должен быть реализован с возможностью расширения функциональности
  • Архитектура должна предусматривать масштабирование решения

Технологический стек


  • В настоящее время для большинства веб проектов используетсяязык JavaScript.Над JavaScript есть надстройкаTypeScript, которая обеспечивает типизацию переменных и реализацию шаблонов.
  • Для полнофункциональных веб проектов есть несколько стандартных стеков технологий. Один из самых популярных: MEVN (MongoDB + Express.js + Vue.js + Node.js), мне больше нравится PEVN (PostgreSQL + Express.js + Vue.js + Node.js), т.к. RDB базы мне технологически ближе, чем NoSQL.
  • Для GUI существует несколькофреймворков (Vue, React, Angular). Тут выбор, скорее, определяется традициями или личными предпочтениями, поэтому сложно говорить, что лучше, а что хуже. Сначала я выбрал Vue.js, да так на нем и остался. Этот фреймворк хорошо развивается, для него есть готовыевизуальныекомпоненты встилеMaterial Design (Vuetify), которые хорошо смотрятся даже при непрофессиональном применении. Считается, что для React порог вхождения выше, чем для Vue. В React сначала надо осознать специфичную объектную модель, которая отличается от классического веб программирования: компоненты как функции, компоненты как классы, цепочки вызовов.
  • Выбор базы данных уже сложнее, чем личные предпочтения. Но для PoC, зачастую, требуется не функциональность или производительность, а простота. Поэтому в примере будем использовать самую простую в мире базу SQLite. В промышленных PoC в качестве SQL базы я использую PostgreSQL, в качестве NoSQL ее же, т.к. запас прочности уPostgreSQL огромен. Прочность, конечно, не бесконечна и когда-нибудь настанет необходимость перехода на специализированную базу, но это отдельная тема для обсуждения.

На самом деле, все современные средства разработки можно развернуть и попробовать достаточно быстро. Поэтому сейчас можно делать осознанный выбор технологий, основанный на практике.
Далее разберем подробно все требования к PoC. Начнем с сервера.

Сервер


HTTP сервер


В качестве HTTP сервера япробовал два варианта:

Express более зрелый, Fastify, говорят, более быстрый. Есть еще варианты серверов, примеры перечислены в статье The Best 10 Node.js Frameworks. Я использовал самый популярный Express.js.

HTTPS запросы


Говоря про HTTP, я всегда подразумеваю HTTPS, т.к. без него крайне сложно построить безопасное взаимодействие между клиентом и сервером. Для поддержки HTTPS надо:
  • Получить SSL (TLS) сертификат
  • На сервере реализовать поддержку HTTPS протокола
  • На клиенте использовать запросы к клиенту с префиксом HTTPS

Получаем сертификат
Для организации работы HTTPS протокола нужен SSL сертификат. Существуют бесплатные сертификаты Lets Encrypt, но солидные сайты получают платные сертификаты от доверительных центров сертификации CA(Certificate Authority). Для наших тестовых целей на локальном хосте достаточно, так называемого, self-signedcertificate. Он генерируется с помощью утилиты openssl:
openssl req -nodes -new -x509 -keyout server.key -out server.crt

Далее отвечаем на несколько вопросов про страну, город, имя, email и т.п. В результате получаемдва файла:
  • server.key ключ
  • server.crt сертификат

Их нужно будет подложить нашему Express серверу.

HTTPS сервер
С Express сервером все достаточно просто, можно взять стандартный пакет HTTPS из Node.js, и использовать официальную инструкцию: How to create an https server?
После реализации HTTPS сервера используем полученные файлы server.key и server.crt и стандартный для HTTPS порт443.

Взаимодействие с внешними системами


Самая известная библиотека для реализации HTTP/HTTPS запросов: Axios. Она используется как в сервере для вызова API внешних систем, так и в клиенте для вызова API сервера. Есть еще варианты библиотек, которые можно использовать дляспецифичных целей: Обзор пяти HTTP-библиотек для веб-разработки.

Взаимодействие с базой данных


Для работы с базой данных я использовал самую популярную библиотеку для Node.js: Sequelize. В ней мне больше всего нравится то, что переключиться между различными типами баз данных можно всего лишь изменив несколько настроечных параметров. При условии, конечно, что в самом коде не используется специфика определенной базы.Например, чтобы переключиться с SQLight на PostgreSQL, надо в конфигурационном файле сервера заменить:
dialect: 'sqlite'

на
dialect: 'postgres'

И при необходимости изменить имя базы, пользователя и хост.
В PoC я использовал базу данныхSQLite, которая не требует установки.Для администрирования баз данных есть хорошо развитые GUI:

Безопасность доступа к данным


В примере реализована следующая простейшая логика доступа к данным, которую можно расширять и изменять в зависимости от бизнес-задач:
  1. При регистрации каждому пользователю присваивается уникальный ID.
  2. Когда пользователь создает новые данные, в них добавляется его ID.
  3. При любом запросе к данным проверяется, что пользователь имеет на это право:

  • Получить данные могут все зарегистрированные пользователи.
  • Изменить и удалить данные может только владелец этих данных.

Такая модель доступа к данным называетсяDiscretionary Access Control (DAC), по-русски: избирательное управление доступом. В этой модели владелец данных может делать с ними любые CRUD операции, а права остальных пользователей ограниченны.
Существуют несколько моделей контроля доступа к данным. ПомимоDiscretionary Access Control (DAC)OWASP рекомендует (Access Control Cheat Sheet) использовать следующие:
  • Role-Based Access Control (RBAC)
  • Mandatory Access Control (MAC)
  • Permission Based Access Control

Безопасность вызовов API


В моей предыдущей статье я рассматривал все возможные угрозы: Безопасность REST API от А до ПИ.В PoC реализованы следующие механизмы:
  • Cross-Site Request Forgery (CSRF) (Межсайтовая подмена запросов)

От данной угрозы реализован механизм CSRF токенов когда для каждой сессии пользователя генерируется новый токен (он же SessionId) и сервер проверяет его валидность при любых запросах с клиента. Алгоритм генерации и проверки SessionId подробно описан далее в разделе Авторизация, аутентификация пользователей.
  • Cross-origin resource sharing (CORS) (Кросс-доменное использование ресурсов)

Защита реализована с помощью пакета CORS. В config файле сервера указываются адрес (origin), с которого могут поступать API запросы и список методов (methods), которые может использовать клиент. В дальнейшем сервер будет автоматически ограничивать прием запросов в соответствии с этими настройками.
Для защиты от угроз, перечисленных далее, я использовал пакетhelmet, который позволяет задать значения для определенных HTTP заголовков:
  • Cross-site Scripting (XSS) (Межсайтовое выполнение скриптов)

Для защиты выставляется заголовок, ограничивающий выполнение скриптов:
X-XSS-Protection: 1; mode=block

  • Insecure HTTP Headers

Блокируется отправка сервером заголовка, дающего дополнительную информацию злоумышленнику:
X-Powered-By: Express

  • Insecure HTTP Headers: HTTP Strict Transport Security (HSTS)

Используется Strict-Transport-Security заголовок, который запрещает браузеру обращаться к ресурсам по HTTP протоколу, только HTTPS:
Strict-Transport-Security: max-age=15552000; includeSubDomains

  • Insecure HTTP Headers: X-Frame-Options (защита от Clickjacking)

Данный заголовок позволяет защититься от атаки Clickjacking. Он разрешает использовать фреймы только в нашем домене:
X-Frame-Options: SAMEORIGIN

  • Insecure HTTP Headers: X-Content-Type-Options

Установка заголовка X-Content-Type-Options запрещает браузеру самому интерпретировать тип присланных файлов и принуждает использовать только тот, что был прислан в заголовке Content-Type:
X-Content-Type-Options: nosniff

Защита от DoS


Необходимо защитить сервер и от отказа в обслуживании (DoS-атаки). Например, ограничить число запросов от одного пользователя или по одному ресурсу в течении определенного времени.
Для Node.js существуют средства, позволяющие автоматически реализовывать ограничения на число запросови сразу посылать ответ 429 Too Many Requests, не нагружая бизнес логику сервера.
В примере реализовано простейшее ограничение на число запросов от каждого пользователя в течении определенного времени по таблице Data. В промышленных системах надо защищать все запросы, т.к. даже один запрос, оставшийся без проверки может дать возможность провести атаку.

Безопасность конфигурационных параметров


Настройки для клиента и сервера хранятся в файлах configв JSON формате. Чувствительные настройки сервера нельзя хранить вconfig файлах, т.к. они могут стать доступны в системах версионного контроля. Для такой информации как: логины/пароли для базы данных, ключи доступа к API и т.д. надо использовать специализированные механизмы:
  • Задавать эти параметры в .env файле. Файл .env прописан в gitignore и не сохраняется в системах версионного контроля, поэтомупоэтому туда можно безопасно записывать чувствительную информацию.
  • Использовать механизм переменных окружения (environment variables), которые устанавливаются вручную или прописываютсяинсталляторами.

Логирование


Стандартные требования к логированию:
  • Обеспечить разный уровень логирования (Debug; Info; Warning; Error и т.д.)
  • Логировать HTTP/HTTPS запросы
  • Дублировать консольный вывод в файлы на диске
  • Обеспечить самоочистку файлов логирования по времени иобъёму

Варианты пакетов для логирования, которые я пробовал:

В проекте я использовал log4js. Пакет log4jsработает интуитивно понятно и достаточно развит, чтобы реализовать все требования.

Мониторинг


Мы не будем останавливаться на внешних система мониторинга: Обзор систем мониторинга серверов, т.к. они начинают играть важную роль не на этапе PoC, а в промышленной эксплуатации.
Обратимся к внутренним системам, которые позволяют наглядно посмотреть, что сейчас происходит с сервером, например пакетexpress-status-monitor. Если его установить, топо endpoint
/monitor

можно наглядно мониторить простейшие параметры работы сервера, такие как загрузка CPU, потребление памяти, http нагрузку и т.п. Приведу скриншот из нашего примера. На скриншоте горит красная плашка Failed и может показаться, что что-то не так. Но, на самом деле, все в порядке, т.к. вызов API сознательно делается на несуществующий endpoint:
/wrongRoutePath

image

База данных


Структурабазы данных


В нашем примере реализованы две таблицы с говорящими именами:
  • Users
  • Data

Лучшие практики говорят, что все проверки надо помаксимуму отдавать базе данных. Поэтому в схеме базы данных аккуратно определяем все типы данных, добавляем ограничения (constraints) и внешние ключи.Для пакета sequelize данная функциональность подробно описана в стандартной документации.
В нашем пример сделан один внешний ключ UserId в таблице Data для того, чтобы гарантировать, что у каждой записи в таблицеData будет определенный владелец User. Это позволит при изменении или удалении записи в Data реализовать проверку пользователя, т.к. по нашей логике данные может удалять только их владелец.
Еще одна особенность нашей схемы в том, что один столбец таблицы Data задан в формате JSON. Такой формат удобно использовать, если внешняя система возвращает данные в JSON. В этом случае можно просто записать полученные данные, не тратя усилия на парсинг,а потом делать SQL запросы, используя расширенный синтаксис.Описание работы со столбцами JSON можно найти в официальной документации баз данных. В качестве независимого описания мне понравилась статья на Хабре, в которой описаны все варианты запросов: "JSONB запросы в PostgreSQL.

Формальная схема DB
Cхема таблицыUsers:
  • id INTEGERPRIMARY KEY AUTOINCREMENT уникальный ID
  • uuid UUID NOT NULL UNIQUE уникальный UUID, по нему идет связь с данными этого пользователя в таблице Data
  • email VARCHAR(255) NOT NULL UNIQUE
  • password VARCHAR(64)
  • ddosFirstRequest DATETIME
  • ddosLastRequest DATETIME
  • ddosRequestsNumber DECIMAL
  • lastLogin DATETIME
  • loginState VARCHAR(255) NOT NULL
  • sessionId VARCHAR(1024)
  • commonToken VARCHAR(1024)
  • googleToken VARCHAR(1024)
  • googleAccessToken VARCHAR(1024)
  • googleRefreshToken VARCHAR(1024)
  • createdAt DATETIME NOT NULL
  • updatedAt DATETIME NOT NULL

Схема таблицыData:
  • id INTEGERPRIMARY KEY AUTOINCREMENT
  • uuid UUID NOT NULL UNIQUE
  • ownerUuid UUID NOT NULL
  • data JSON NOT NULL
  • UserId INTEGER REFERENCES Users (id) ON DELETE CASCADE ON UPDATE CASCADE,
  • createdAt DATETIME NOT NULL
  • updatedAt DATETIME NOT NULL


Начальные значения


  • В таблице Users задан один пользовательuser@example.comс паролем password.
  • В таблице Data записано несколько предопределенных значений, которые можно получить из GUI.

Далее перейдем к клиенту.

Клиент


HTTPS запросы


Для безопасной передачи информации клиент использует HTTPS протокол. Для запросов используется та же библиотека, что и на сервере для вызова API внешних систем: Axios.

RESTful API


REST API лучше реализовывать, используя стандартные средства, чтобы было проще тестировать и документировать, например SwaggerHub
Наше REST API сформировано по правилам Open API и использует стандартные HTTP методы для манипуляции объектами из таблиц Users и Data.В схеме нашего API не все сделано по чистым правилам REST, но главное соблюдена общая концепция.
Хорошей практикой являетсяограничение объёма возвращаемых данных. Для этого используются параметры в запросе: фильтр (filter), ограничение (limit) и смещение (offset). В примере этого нет, но в промышленных системах эта функциональность должна быть реализована.Но даже если реализованы ограничивающие механизмы на клиенте, должна осуществляться дополнительная проверка на сервере на максимальные значения. В нашем примере в конфигурации сервера реализован ограничитель на максимальное число возвращаемых строк из базы,который подставляется вSELECT запросы:limit: 1000

YAML файл с Open API описанием
---swagger: "2.0"info:  description: "This is a common-test-db API. You can find out more about common-test-db\    \ at \nhttps://github.com/AlexeySushkov/common-test-db\n"  version: "2.0.0"  title: "common-test-db"  contact:    email: "alexey.p.sushkov@gmail.com"  license:    name: "MIT License"    url: "https://github.com/AlexeySushkov/common-test-db/blob/main/LICENSE"host: "localhost:443"basePath: "/commontest/v1"tags:- name: "data"  description: "Everything about Data"  externalDocs:    description: "Find out more"    url: "https://github.com/AlexeySushkov/common-test-db/"- name: "users"  description: "Everything about Users"  externalDocs:    description: "Find out more"    url: "https://github.com/AlexeySushkov/common-test-db/"schemes:- "https"paths:  /data:    get:      tags:      - "data"      summary: "Gets all data"      description: "Gets all data"      operationId: "getData"      produces:      - "application/json"      parameters: []      responses:        "200":          description: "successful operation"          schema:            type: "array"            items:              $ref: "#/definitions/Data"        "400":          description: "Invalid status value"      x-swagger-router-controller: "Data"    post:      tags:      - "data"      summary: "Add a new data to the db"      operationId: "addData"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "Data object that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/Data"      responses:        "500":          description: "Create data error"      x-swagger-router-controller: "Data"    put:      tags:      - "data"      summary: "Update an existing data"      operationId: "updateData"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "Data information that needs to be changed"        required: true        schema:          $ref: "#/definitions/Data"      responses:        "400":          description: "Invalid uuid"        "404":          description: "User not found"        "500":          description: "Update data error"      x-swagger-router-controller: "Data"    delete:      tags:      - "data"      summary: "Delete an existing data"      operationId: "deleteData"      consumes:      - "application/x-www-form-urlencoded"      - "application/json"      produces:      - "application/json"      parameters:      - name: "uuid"        in: "formData"        description: "uuid"        required: true        type: "string"      responses:        "400":          description: "Invalid uuid"        "404":          description: "User not found"        "500":          description: "Delete data error"      x-swagger-router-controller: "Data"  /users:    post:      tags:      - "users"      summary: "Register new user"      operationId: "userRegister"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be deleted"        required: true        schema:          $ref: "#/definitions/User"      responses:        "500":          description: "Register user error"      x-swagger-router-controller: "Users"    put:      tags:      - "users"      summary: "Update existing user"      operationId: "userUpdate"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User information that needs to be changed"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Delete user error"      x-swagger-router-controller: "Users"    delete:      tags:      - "users"      summary: "Delete user"      operationId: "userDelete"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Delete user error"      x-swagger-router-controller: "Users"  /users/login:    post:      tags:      - "users"      summary: "Login"      operationId: "userLogin"      consumes:      - "application/json"      produces:      - "application/json"      parameters:      - in: "body"        name: "body"        description: "User that needs to be added to the db"        required: true        schema:          $ref: "#/definitions/User"      responses:        "404":          description: "User not found"        "500":          description: "Login user error"      x-swagger-router-controller: "Users"definitions:  Data:    type: "object"    required:    - "Counter1"    - "Counter2"    properties:      Counter1:        type: "integer"        format: "int64"      Counter2:        type: "integer"        format: "int64"    example:      Counter1: 10      Counter2: 20  User:    type: "object"    required:    - "email"    - "password"    properties:      email:        type: "string"      password:        type: "string"    example:      password: "password"      email: "email"  ApiResponse:    type: "object"    properties:      status:        type: "string"


Интересная возможность встроить Swagger прямо в сервер, далее подложить ему YAML, реализующий API и по endpoint:
/api-docs

получить стандартную Swagger панель управления и осуществлять тестирование и просмотр документации. Скриншот Swagger, встроенного в наш сервер:

image

GraphQL


Существуют и альтернативы REST API, например GraphQL. Данный стандарт разработан Facebook и позволяет избавиться от недостатков REST:
  • В REST API взаимодействие происходит с использованием многочисленные endpoints. В GraphQL одна.
  • В REST API для получения данных от разных endpoints необходимо осуществить множественные запросы. В GraphQL все необходимые данные можно получить одним запросом.
  • В REST API клиент не может контролировать объём и содержимое данных в ответе. В GraphQL набор возвращаемых данных предсказуем, т.к. задается в запросе. Это сразу избавляет от угрозы: Excessive Data Exposure (Разглашение конфиденциальных данных)
  • В REST API отсутствует обязательность формальной схемы. В GpaphQL создание схемы API с определенными типами данных обязательно, при этом автоматически получается документация и сервер для тестирования (самодокументироваемость и самотестируемость).
  • Клиент всегда может запросить схему API с сервера, тем самым синхронизировать формат своих запросов
  • Из коробки работают подписки, реализованные на технологии WebSockets

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

Регистрация,аутентификация и авторизация пользователей


Если приложение доступно из Интернета, то в форме регистрации имеет смысл использовать функциональность reCaptсha от Google для защиты от ботов и DoS атак. В нашем примере я использую пакет vue-recaptcha.
Для использованияreCaptсha нужно на сайте Google зарегистрировать приложение, получить sitekey и прописать его в настройках клиента. После этого на форме появится, известный всем, вопрос про робота:
image

В примере реализована регистрация пользователей с помощью логина/пароля ис использованием Google Account.
  • При регистрации по логин/пароль пароли в базе данных, разумеется, не хранятся в виде текста, а хранится только хеш. Для его генерации используется библиотекаbcrypt, которая реализует алгоритм хеширванияBlowfish. Сейчас уже есть мнение, что надо использовать более надежные алгоритмы: Password Hashing: Scrypt, Bcrypt and ARGON2.
  • При регистрации с помощью Google Account организация хранения аутентификационных данных проще, т.к. в базу записывается только token, полученный от Google.

Для реализации различных схем аутентификации есть библиотека Passport, которая упрощает работу и скрывает детали. Но чтобы понять как работают алгоритмы, в примере все сделано в соответствии со стандартами, которые я уже описывал в своей статье: Современные стандарты идентификации: OAuth 2.0, OpenID Connect, WebAuthn.
Далее в статье разберем практическую реализацию.

Аутентификация по логину/паролю (Basic Authentication)
Тут все достаточно просто. При получении запроса на логин сервер проверяет, что пользователь с присланнымemail существует и что хеш полученного пароля совпадает с хешом из базы. Далее:
  • Сервер формирует SessionId в формате UUID и Token в формате JWT (JSON Web Token)
  • Токену присваиватся время жизни, которое определяется бизнес задачами. Например, Google устанавливет своим токенам время жизни 1 час.
  • Для формирования токена используется пакетjsonwebtoken
  • В JWT помещается email пользователя, который подписывается ключом сервера. Ключ это текстовая строка, которая хранится, как конфигурационный параметр на сервере.Содержимое токена кодируется Base64
  • SessionId сохраняется в базе и отправляется клиенту в заголовке X-CSRF-Token
  • Token сохраняется в базе и отправляется клиенту в теле HTTP ответа
  • Получив SessionId клиент сохраняетSessionId в sessionStorageбраузера
  • Token записывается в localStorage или Сookiesбраузерадля реализации возможности запомнить меня на этом устройстве
  • В дальнейшем клиент будет присылать SessionId в HTTP заголовкеX-CSRF-Token:

X-CSRF-Token: 69e530cf-b641-445e-9866-b23c492ddbab

  • Token будет присылаться в заголовкеHTTP Authorization с префиксомBearer:

Authorization: BearereyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MDYxNTY3MTUsImV4cCI6MTYwNjc2MTUxNX0.h3br5wRYUhKIFs3SMN2ZPvMcwBxKn7GMIjJDzCLm_Bw

  • Сервер каждый раз проверяетSessionId на то, что сессия существует и связана с пользователем. Таким образом, гарантируется, что запрос приходит от залогиненного клиента, а не стороннего сайта злоумышленника.
  • Сервер при каждом запросе будет проверять присланныйToken на валидность:

  1. Токен подписан сервером;
  2. Пользователь с email существует;
  3. Время жизни токена не истекло. Если же оно истекло, то при выполнении предыдущих условий автоматически формируется новый токен.

В общем случае излишне генерировать и SessionId и Token. В нашем PoC для примера реализованы оба механизма. Посмотрим внимательней на наш JWT. Пример закодированного JWT токена из заголовка:
Authorization:yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InRlc3RAdGVzdC5jb20iLCJpYXQiOjE2MDYxNTY3MTUsImV4cCI6MTYwNjc2MTUxNX0.h3br5wRYUhKIFs3SMN2ZPvMcwBxKn7GMIjJDzCLm_Bw

Раскодируем его открытыми средствами. Например самый простой способ загрузить на сайтhttps://jwt.io

image

Мы видим, что информация, помещенная вJWT не зашифрована, а только подписана ключом сервера и туда нельзя помещать чувствительные к разглашению данные!

Аутентификация с помощью Google Account (Token-Based Authentication)
Тут посложнее, поэтому нарисую диаграмму:

image

1. Пользователь заходит на GUI PoC, выбирает Login with Google".
2. Клиент запрашивает у сервера SessionId и настройки Google. Настройки надо предварительно получить с сайта Google, например, по инструкции из моей статьи: Современные стандарты идентификации: OAuth 2.0, OpenID Connect, WebAuthn
3. Клиент сохраняет SessionId в sessionStorageбраузера
4. Из браузера формируется GET запрос на GoogleAuthorization Server (красная стрелка). Для понимания алгоритма, надо обратить внимание, что ответ на этот запрос браузер получит от Google только в самом конце call flow на шаге 13 и это будет 307 (Redirect).

Формат GET запроса:
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=918876437901-g312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com&scope=openid+email&redirect_uri=http://localhost:8081/redirect&access_type=offline&state=450c2fd9-0a5e-47d2-8ed5-dd1ff4670b58

В этом запросе:
  • accounts.google.com/o/oauth2/v2/auth endpoint для начала аутентификации. У Google есть, кстати, адрес, по которому можно посмотреть актуальный список всех Google API endpoints:https://accounts.google.com/.well-known/openid-configuration
  • response_type=code параметр говорит, что ожидаем получить в ответAuthorization Code
  • client_id Client ID, выданный при регистрации приложения на Google (в примере указан не настоящий)
  • scope=openid email к каким данным пользователя мы хотим получить доступ
  • redirect_uri = localhost:8081/redirectCallback адрес, заданный при регистрации приложения. В нашем случае это адрес на нашем сервере, который получит запрос от Google
  • state = SessionId, который передается между клиентом, Google и сервером для защиты от вмешательства внешнего злоумышленника

5. Google Authorization Server показывает стандартную Google форму логина.
6. Пользователь вводит свои логин/пароль от Google аккаунта.
7. Google проверяет пользователя и делает GET запрос на адрес Callback с результатом аутентификации, Authorization Code в параметре code и нашем SessionId в параметреstate:
  • state: '450c2fd9-0a5e-47d2-8ed5-dd1ff4670b58',
  • code: '4/0AY0e-g4pg_vSL1PwUWhfDYj3gpiVPUg20qMkTY93JYhmrjttedYwbH376D_BvzZGmjFdmQ',
  • scope: 'email openid www.googleapis.com/auth/userinfo.email',
  • authuser: '1',
  • prompt: 'consent'

8. Сервер, не отправляя пока ответ на GET, формирует POST запрос с Authorization Code, Client ID и Client Secret:
POSThttps://oauth2.googleapis.com/token?code=4/0AY0e-g4pg_vSL1PwUWhfDYj3gpiVPUg20qMkTY93JYhmrjttedYwbH376D_BvzZGmjFdmQ&client_id=918876437901-g312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com&client_secret=SUmydv3-7ZDTIh8аНК85chTOt&grant_type=authorization_code&redirect_uri=http://localhost:8081/redirect

  • oauth2.googleapis.com/token это endpoint для получения token
  • code только что присланный code
  • client_id Client ID, выданный при регистрации приложения на Google (в примере указан не настоящий)
  • client_secret Client Secret, выданный при регистрацииприложения на Google (в примере указан не настоящий)
  • grant_type=authorization_code единственно возможное значение из стандарта

9. Google проверяет присланные данные и формирует access token в формате JWT (JSON Web Token), подписанный своим приватным ключом. В этом же JWT может содержаться и refresh token, c помощью которого возможно продолжение сессии после ее окончания:
  • access_token: 'ya29.a0AfH6SMBH70l6wUe1i_UKfjJ6JCudA_PsIIKXroYvzm_xZjQrCK-7PUPC_U-3sV06g9q7OEWcDWYTFPxoB1StTpqZueraUYVEWisBg46m1kQAtIqhEPodC-USBnKFIztGWxzxXFX47Aag',
  • expires_in: 3599,
  • refresh_token: '1//0cAa_PK6AlemYCgYIARAAGAwSNwF-L9IrdUt1gzglxh5_L4b_PwoseFlQA1XDhqte7VMzDtg',
  • scope: 'openid www.googleapis.com/auth/userinfo.email',
  • token_type: 'Bearer',
  • id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImRlZGMwMTJkMDdmNTJhZWRmZDVmOTc3ODRlMWJjYmUyM2MxOTcfobDCf2VrxXb6CCxoL_dZq1WnlEjBZx_Sf6Rg_tn3x4gWtusO1oe_bJx_gvlSLxtvSOdO_kPB2uGGQHr3xzF_Evr-S-BiGS8zMuIkslyN6fU7P7BdNVyOYAIYFvHikyIpAoesV2Fd2yBSngBwGmWfrHL7Z2415UrnlCG4H1Nw'

10. Сервер валидирует пришедший Token. Для этого нужен открытый ключ от Google. Актуальные ключи находятся по определенному адресу и периодически меняются:https://www.googleapis.com/oauth2/v1/certs.Поэтому надо или получать их каждый раз по этому адресу, либо хардкодить и следить за изменениями самому.Декодируем пришедший token, для нас самое главное это подтвержденный email пользователя:
decodedIdToken: {iss: 'https://accounts.google.com',azp: '918962537901-gi8oji3qk312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com',aud: '918962537901-gi8oji3qk312pqdhg5bcju4hpr3efknv.apps.googleusercontent.com',sub: '101987547227421522632',email: 'work.test.mail.222@gmail.com',email_verified: true,at_hash: 'wmbkGMnAKOnfAKtGQFpXQw',iat: 1606220748,exp: 1606224348}

11. Проверяем, что email пользователя существует, или создаём нового пользователя c email из токена. При этомSessionId и полученные от Google токены сохраняются в базе.
12. Только теперь отвечаем Google HTTP кодом 307 (Redirect) и заголовком Location c адресом на клиенте:
HTTP Location:http://localhost:8080/googleLogin

13. И только теперь Google отвечает браузеру с тем же кодом307 (Redirect) и заголовком Location с заданным нами адресом
14. Браузер переходит на адрес, указанный вLocation иклиент определяет, что произошла успешная аутентификация пользователя с помощью Google аккаунта
15. Клиент, по сохраненному в sessionStorage SessionId, получает на сервере токен и данные пользователя
16. Клиент сохраняет токен в localStorage браузера

На этом процедура аутентификации с помощью Google аккаунта завершена и можно переходить к штатной работе приложения.

Прием, обработка и визуализация данных


Сделаем стандартный вид приложения, как рекомендует Google:
  • Drawer (navigation-drawer) в левой стороне
  • Меню сверху (v-app-bar)
  • Footer внизу (v-footer)
  • Для визуализации полученных данных используем карточки (Data Cards)

image

Для создания новых данных используется простая форма ввода:

image

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

  1. Для Vue есть библиотекаVuex, реализующаяstate management pattern.
  2. Есть универсальная библиотека Redux основанная на Flux от Facebook. Она реализует концепцию состояний, используя понятия action, state, view.

  • В примере я использовал Vuex.
  • Также необходимRouter для реализации переходов между страницами.

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

Архитектура


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

Заключение


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

Исходники на Github:https://github.com/AlexeySushkov/common-test-db
Пример запущен в облаке Amazon, можно посмотреть, как он выглядит вживую:http://globalid.tech/

Остается только в канун Нового Года пожелать, чтобы сбылись все ваши самые заветные PoC!

It's only the beginning!
Подробнее..

Онлайн-митап сообщества разработчиков MSK VUE.JS

20.07.2020 14:14:41 | Автор: admin
image

23 июля приглашаем на онлайн-митап сообщества разработчиков MSK VUE.JS.

В программе митапа:

  • Разработка конструктора отчетов c Cube.js;
  • 5 действенных техник оптимизации vue-приложений;
  • Решение проблем REST API при помощи GraphQL.

Зарегистрироваться

О митапе


Разработчик Cube.js Леонид Яковлев расскажет, как сделать конструктор отчетов при помощи cube.js на бэкенде. Подробно поговорит, что такое query builder, какие у него преимущества и покажет пример простого queryBuilder

Руководитель AFFINAGE Игорь Яковлев поделится собственными техниками, инсайтами и лайфхаками, как разрабатывать по-настоящему быстрые vue-приложения.

Разработчик Московской биржи Юлия Кузнецова расскажет, как GraphQL помогает решать проблемы REST API архитектуры и покажет, как это сделать на примере Vue Apollo.

Об экспертах


Леонид Яковлев разработчик в команде Developer Relations and Community фреймворка Cube.js.

Игорь Яковлев руководитель аутсорспродакшена по фронтенду AFFINAGE.

Юлия Кузнецова старший разработчик Московской биржи.

О MSK VUE.JS


MSK VUE.JS сообщество разработчиков Vue.js, которое регулярно проводит митапы, чтобы делиться опытом, обсуждать перспективы и строить комьюнити.

По теме:


Подробнее..

Практическое знакомство с Deno разрабатываем REST API MongoDB Linux

26.01.2021 00:16:12 | Автор: admin

Всем привет. В этот раз я решил сделать нечто более интересное, чем очередной бот, поэтому далее я покажу как реализовать REST API с Deno, подключить и использовать MongoDB в качестве базы данных, и всё это запустить из под Linux.

Видео версия данной заметки доступна ниже:

Описание задачи

В качестве примера я выбрал Github Gists API и следующие методы:

  • [POST] Create a gist;

  • [GET] List public gists;

  • [GET] Get a gist;

  • [PATCH] Update a gist;

  • [DELETE] Delete a gist.

Создание проекта

Для начала мы добавляем файл api/mod.ts :

console.log('hello world');

И проверяем, что всё работает командой deno run mod.ts:

mod.tsmod.ts

Добавление зависимостей

Создаём файл api/deps.ts и добавляем следующие зависимости:

  • Пакет oak для работы с API;

  • Пакет mongo для работы с MongoDB;

/* REST API */export { Application, Router } from "<https://deno.land/x/oak/mod.ts>";export type { RouterContext } from "<https://deno.land/x/oak/mod.ts>";export { getQuery } from "<https://deno.land/x/oak/helpers.ts>";/* MongoDB driver */export { MongoClient, Bson } from "<https://deno.land/x/mongo@v0.21.0/mod.ts>";

Отступление: В отличие от NodeJS, авторы Deno отказались от поддержки npm и node_modules, а необходимые библиотеки подключаются по url и кешируются локально. Сами библиотеки можно найти в разделе Third Party Modules на сайте http://deno.land.

Добавление API Boilerplate

Далее, добавляем код для запуска API в файл mod.ts:

import { Application, Router } from "./deps.ts";const router = new Router();router  .get("/", (context) => {    context.response.body = "Hello world!";  });const app = new Application();app.use(router.routes());app.use(router.allowedMethods());await app.listen({ port: 8000 });

Причём функции Application и Router импортируем уже из локального файла deps.ts.

Проверим, что всё было сделано верно:

  • Запускаем приложение командой deno run --allow-net mod.ts;

  • Открываем в браузере http://localhost:8000;

  • Получаем страницу с сообщением 'Hello world!';

Отступление: Deno позиционируется как secure by default. Другими словами, у запускаемого приложения (скрипта) не будет доступа к сети (--allow-net, файловой системе (--allow-readи --allow-write, параметрам окружения (--allow-env) пока этот доступ явно не разрешён.

Добавление метода POST /gists

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

Прежде всего опишем контракт:

  • [POST] /gists

  • Параметры:

    • content: string | body;

  • Ответы:

    • 201 Created;

    • 400 Bad Request;

Обработчик

Добавляем папку handlers и файл create.ts, в котором будет расположен handler (обработчик) запроса:

import { RouterContext } from "../deps.ts";import { createGist } from "../service.ts";export async function create(context: RouterContext) {  if (!context.request.hasBody) {    context.throw(400, "Bad Request: body is missing");  }  const body = context.request.body();  const { content } = await body.value;  if (!content) {    context.throw(400, "Bad Request: content is missing");  }  const gist = await createGist(content);  context.response.body = gist;  context.response.status = 201;}

В этой функции мы:

  • Валидируем входные значения (request.hasBody и !content);

  • Вызываем функцию createGist нашего сервиса (добавим далее);

  • Возвращаем добавленный объект в ответе и 201 Created.

Сервис

Далее, нам необходимо передать управление из обработчика в сервис (добавляем service.ts):

import { insertGist } from "./db.ts";export async function createGist(content: string): Promise<IGist> {  const values = {    content,    created_at: new Date(),  };  const _id = await insertGist(values);  return {    _id,    ...values,  };}interface IGist {  _id: string;  content: string;  created_at: Date;}

В данном случае функция принимает единственный аргумент content: string и возвращает объект, структура которого описывается интерфейсом IGist.

Репозиторий

Последним этапом обработки запроса является сохранение записи в MongoDB. Для этого мы добавляем файл db.ts и соответствующую функцию:

import { Collection } from "<https://deno.land/x/mongo@v0.21.0/src/collection/collection.ts>";import { Bson, MongoClient } from "./deps.ts";async function connect(): Promise<Collection<IGistSchema>> {  const client = new MongoClient();  await client.connect("mongodb://localhost:27017");  return client.database("gist_api").collection<IGistSchema>("gists");}export async function insertGist(gist: any): Promise<string> {  const collection = await connect();  return (await collection.insertOne(gist)).toString();}interface IGistSchema {  _id: { $oid: string };  content: string;  created_at: Date;}

В этом файле мы:

  • Импортируем необходимые типы и функции для работы с MongoDB;

  • Подключаемся к базе данных gist_api в функции connect;

  • Описываем формат объектов, которые хранятся в коллекции gist_api интерфейсом IGistSchema;

  • Сохраняем объект методом insertOne и возвращаем его идентификатор (inserted id);

Запускаем экземпляр MongoDB

Далее мы запускаем терминал, запускаем и проверяем статус нашей базы данных следующими командами:

sudo systemctl start mongodsudo systemctl status mongod

Если всё было сделано верно, то получим следующий результат:

Отступление: Как установить MongoDB на Ubuntu

Запускаем приложение

  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;

  • Открываем Postman и вызываем метод нашего API:

Если всё сделано верно, то в качестве ответа получаем 201 Created и сохранённый объект с проставленным _id:

Отступление: Как вы могли заметить, в процессе разработки мы используем TypeScript без транспайлеров. Причина проста - Deno поддерживает TypeScript из коробки.

Добавление метода GET /gists

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

Прежде всего опишем контракт:

  • [GET] /gists

  • Параметры:

    • skip: string | query;

    • limit: string | query;

  • Ответы:

    • 200 OK;

Обработчик

Добавляем файл handlers/list.ts, в котором будет расположен handler (обработчик) запроса:

import { getQuery, RouterContext } from "../deps.ts";import { getGists } from "../service.ts";export async function list(context: RouterContext) {  const { skip, limit } = getQuery(context);  const gists = await getGists(+skip || 0, +limit || 0);  context.response.body = gists;  context.response.status = 200;}

В этой функции мы:

  • Получаем параметры с query string с помощь функции getQuery;

  • Вызываем функцию getGists нашего сервиса (добавим далее);

  • Возвращаем массив найденных объектов в ответе и 200 OK;

Отступление: Функция сервиса будет принимать аргументы типа number, в то время как в обработчик к нам приходят параметры типа string. Для этого мы делаем приведение типов следующей конструкцией +skip || 0 (корректные значения конвертируются, некорректные приводятся к NaN и игнорируются в пользу 0).

Сервис

Далее, передаём управление из обработчика в сервис:

export function getGists(skip: number, limit: number): Promise<IGist[]> {  return fetchGists(skip, limit);}

В данном случае функция принимает аргументы skip: number и limit: number, и возвращает массив объектов, структура которых описывается интерфейсом IGist.

Репозиторий

Последним этапом обработки запроса является получение записей из MongoDB. Для этого мы добавляем функцию fetchGists в файл db.ts:

export async function fetchGists(skip: number, limit: number): Promise<any> {  const collection = await connect();  return await collection.find().skip(skip).limit(limit).toArray();}

В этой функции мы:

  • Подключаемся к базе данных gist_api в функции connect;

  • Получаем все записи коллекции, пропускаем skip из них и возвращаем в кол-ве limit;

Запускаем приложение

  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;

  • Открываем Postman и вызываем метод нашего API:

Если всё сделано верно, то в качестве ответа получаем 200 OK и массив ранее добавленных объектов:

Добавление метода GET /gists/:id

Следующим методом мы получаем запись из базы данных по её идентификатору.

Прежде всего опишем контракт:

  • [GET] /gists/:id

  • Параметры:

    • id: string | path

  • Ответы:

    • 200 OK;

    • 400 Bad Request;

    • 404 Not Found.

Обработчик

Добавляем файл handlers/get.ts, в котором будет расположен handler (обработчик) запроса:

import { RouterContext } from "../deps.ts"import { getGist } from "../service.ts";export async function get(context: RouterContext) {    const { id } = context.params;    if(!id) {        context.throw(400, "Bad Request: id is missing");    }    const gist = await getGist(id);    if(!gist) {        context.throw(404, "Not Found: the gist is missing");    }    context.response.body = gist;    context.response.status = 200;}

В этой функции мы:

  • Проверяем наличие id и возвращаем 400 если он отсутствует;

  • Запрашиваем объект в базе данных функцией getGist и возвращаем 404 если он не найден (добавим далее);

  • Возвращаем найденный объект и 200 OK;

Сервис

Далее, передаём управление из обработчика в сервис:

export function getGist(id: string): Promise<IGist> {    return fetchGist(id);}interface IGist {  _id: string;  content: string;  created_at: Date;}

В данном случае функция принимает аргумент id: string и возвращает объект, структура которого описывается интерфейсом IGist.

Репозиторий

Последним этапом обработки запроса является получение записи из MongoDB. Для этого мы добавляем функцию fetchGist в файл db.ts:

export async function fetchGist(id: string): Promise<any> {  const collection = await connect();  return await collection.findOne({ _id: new Bson.ObjectId(id) });}

В этой функции мы:

  • Подключаемся к базе данных gist_api в функции connect;

  • Используем метод findOne для поиска записи удовлетворяющей фильтру по _id;

Запускаем приложение

  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;

  • Открываем Postman и вызываем метод нашего API:

Если всё сделано верно, то в качестве ответа получаем 200 OK и ранее добавленный объект:

Добавление метода PATCH /gists/:id

Следующим методом мы обновляем запись в базе данных по её идентификатору.

Как и прежде, начинаем с контракта:

  • [PATCH] /gists/:id

  • Параметры:

    • id: string | path

    • content: string | body

  • Ответы:

    • 200 OK;

    • 400 Bad Request;

    • 404 Not Found.

Обработчик

Добавляем файл handlers/update.ts, в котором будет расположен handler (обработчик) запроса:

import { RouterContext } from "../deps.ts";import { getGist, patchGist } from "../service.ts";export async function update(context: RouterContext) {  const { id } = context.params;  if (!id) {    context.throw(400, "Bad Request: id is missing");  }  const body = context.request.body();  const { content } = await body.value;  if (!content) {    context.throw(400, "Bad Request: content is missing");  }  const gist = await getGist(id);  if (!gist) {    context.throw(404, "Not Found: the gist is missing");  }  await patchGist(id, content);  context.response.status = 200;}

В этой функции мы:

  • По аналогии проверяем наличие id и возвращаем 400 если он отсутствует;

  • Валидируем входное значение !content;

  • Запрашиваем объект в базе данных функцией getGist и возвращаем 404 если он не найден;

  • Обновляем объект в базе данных функцией patchGist (добавим далее);

  • Возвращаем 200 OK.

Сервис

Далее, передаём управление из обработчика в сервис:

export async function patchGist(id: string, content: string): Promise<any> {  return updateGist({ id, content });}interface IGist {  _id: string;  content: string;  created_at: Date;}

В данном случае функция принимает аргументы id: string и content: string, и возвращает any.

Репозиторий

Последним этапом обработки запроса является обновлении записи в MongoDB. Для этого мы добавляем функцию updateGist в файл db.ts:

export async function updateGist(gist: any): Promise<any> {  const collection = await connect();  const filter = { _id: new Bson.ObjectId(gist.id) };  const update = { $set: { content: gist.content } };  return await collection.updateOne(filter, update);}

В этой функции мы:

  • Подключаемся к базе данных gist_api в функции connect;

  • Описываем фильтр filter объектов, которые мы хотим обновить;

  • Описываем инструкцию update, которую применяем для обновления найденных объектов;

  • Используем метод updateOne собрав всё воедино;

Запускаем приложение

  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;

  • Открываем Postman и вызываем метод нашего API:

Если всё сделано верно, то в качестве ответа получаем 200 OK:

Добавление метода DELETE /gists/:id

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

По традиции, начинаем с контракта:

  • [DELETE] /gists/:id

  • Параметры:

    • id: string | path

  • Ответы:

    • 204 No Content;

    • 404 Not Found.

Обработчик

Добавляем файл handlers/remove.ts, в котором будет расположен handler (обработчик) запроса:

import { RouterContext } from "../deps.ts";import { getGist, removeGist } from "../service.ts";export async function remove(context: RouterContext) {  const { id } = context.params;  if (!id) {    context.throw(400, "Bad Request: id is missing");  }  const gist = await getGist(id);  if (!gist) {    context.throw(404, "Not Found: the gist is missing");  }  await removeGist(id);  context.response.status = 204;}

В этой функции мы:

  • По аналогии проверяем наличие id и возвращаем 400 если он отсутствует;

  • Запрашиваем объект в базе данных функцией getGist и возвращаем 404 если он не найден;

  • Удаляем объект из базы данных функцией removeGist (добавим далее);

  • Возвращаем 204 No Content.

Сервис

Далее, передаём управление из обработчика в сервис:

export function removeGist(id: string): Promise<number> {  return deleteGist(id);}

В данном случае функция принимает единственный аргумент id: string и возвращает number.

Репозиторий

Последним этапом обработки запроса является удаление записи из коллекции MongoDB. Для этого мы добавляем функцию deleteGist в файл db.ts:

export async function deleteGist(id: string): Promise<any> {  const collection = await connect();  return await collection.deleteOne({ _id: new Bson.ObjectId(id) });}

В этой функции мы:

  • Подключаемся к базе данных gist_api в функции connect;

  • Используем метод deleteOne для удаления объекта удовлетворяющего фильтру по _id;

Запускаем приложение

  • Компилируем и запускаем приложение командой deno run --allow-net mod.ts;

  • Открываем Postman и вызываем метод нашего API:

Если всё сделано верно, то в качестве ответа получаем 204 No Content:

Отступление: В данном случае фактическое удаление объекта из коллекции выбрано для наглядности. В реальных приложениях я предпочитаю добавить и обновлять у объекта поле isDeleted: boolean.

FAQ

Вызывая методы API я всегда получаю только 404 Not Found

Убедитесь что вы не забыли сконфигурировать router в файле mod.ts соответствующими обработчиками:

import { Application, Router } from "./deps.ts";import { list } from "./handlers/list.ts";import { create } from "./handlers/create.ts";import { remove } from "./handlers/remove.ts";import { get } from "./handlers/get.ts";import { update } from "./handlers/update.ts";const app = new Application();const router = new Router();router  .post("/gists", create)  .get("/gists", list)  .get("/gists/:id", get)  .delete("/gists/:id", remove)  .patch("/gists/:id", update);app.use(router.routes());app.use(router.allowedMethods());await app.listen({ port: 8000 });

Вызывая методы API я получаю 500 Internal Server Error

Отловить ошибку можно следующим способом:

const app = new Application();app.use(async (ctx, next) => {  try {    await next();  } catch (err) {    console.log(err);  }});...

Ссылки

Заключение

Спасибо за то что дочитали до конца.

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

Подробнее..
Категории: Javascript , Typescript , Node.js , Linux , Deno , Tutorial , Rest api , Mongodb , Ubunty

Автоматизируй это, или Контейнерные перевозки Docker для WebRTC

11.06.2021 08:06:47 | Автор: admin

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

Представим ситуацию: нужно разворачивать много однотипных серверов, причем делать это быстро. Быстро разворачивать, быстро сворачивать. Например, разворачивать тестовые стенды для разработчиков. Когда разработка ведётся параллельно нужно разделить разработчиков, что бы они не мешали друг-другу и возможные ошибки одного из них не блокировали работу остальных.

Путей решения этой задачи может быть несколько:

  1. Использовать виртуальные машины. Несколько тяжеловесное решение. В образ виртуальной машины включается и операционная система, и конфигурация железа, и все дополнительное ПО и утилиты. Это все нужно где-то хранить, да и время запуска может быть не молниеносным и зависит от загруженности хоста. Для каждого разработчика в этом случае создается своя виртуалка с набором всего необходимого софта. Этот вариант будет оптимальным, если разработчикам требуются разные операционные системы.

  2. Использовать скрипты. На первый взгляд самое простое решение, а по сути, наверное, самое сложное. В этом случае, мы не тащим с собой операционку и дополнительное ПО. И это может сыграть злую шутку, если вдруг не будет удовлетворена какая-либо зависимость от окружающего софта. Попадется случайно в репозитории условный Python не той версии и все!

  3. Запускать основной продукт в контейнерах. Это самое современное на сегодняшний день решение. Контейнер - это некая изолированная от внешних факторов среда. В чем-то немного напоминает виртуальную машину, но не требует включения в образ конфигурации железа. В работе так же, как и виртуальная машина, использует ресурсы хоста. Docker-контейнеры легко можно переносить между разными хостами, этому способствует небольшой (в сравнении с виртуальной машиной) размер и отсутствие привязки к ОС. Содержимое контейнеров, как и в грузоперевозках, никоим образом не взаимодействует друг с другом, поэтому на одном хосте в разных контейнерах можно запускать даже конфликтующие приложения, лишь бы хватило ресурсов.

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

Стриминг без использования контейнеров:

Стриминг с использованием контейнеров:

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

  • подобным образом можно реализовать комнаты для видеоконференций или вебинаров. Одна комната - один контейнер. ;

  • организовать систему видеонаблюдения за домами. Один дом - один контейнер;

  • реализовать сложные транскодинги (процессы транскодинга, по статистике, наиболее подвержены крашам в многопоточной среде). Один транскодер - один контейнер.

    и т.п.

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

Почему все таки контейнеры, а не виртуалки?

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

Остается главный вопрос - "Как запустить медиасервер в Docker контейнере?

Разберем на примере Web Call Server.

Легче легкого!

В Docker Hub уже загружен образ Flashphoner Web Call Server 5.2.

Развертывание WCS сводится к двум командам:

  1. Загрузить актуальную сборку с Docker Hub

    docker pull flashponer/webcallserver
    
  2. Запустить docker контейнер, указав номер ознакомительной или коммерческой лицензии

    docker run \-e PASSWORD=password \-e LICENSE=license_number \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest
    

    где:

    PASSWORD - пароль на доступ внутрь контейнера по SSH. Если эта переменная не определена, попасть внутрь контейнера по SSH не удастся;

    LICENSE - номер лицензии WCS. Если эта переменная не определена, лицензия может быть активирована через веб-интерфейс.

Но, если бы все было настолько просто не было бы этой статьи.

Первые сложности

На своей локальной машине с операционной системой Ubuntu Desktop 20.04 LTS я установил Docker:

sudo apt install docker.io

Создал новую внутреннюю сеть Docker с названием "testnet":

sudo docker network create \ --subnet 192.168.1.0/24 \ --gateway=192.168.1.1 \ --driver=bridge \ --opt com.docker.network.bridge.name=br-testnet testnet

Cкачал актуальную сборку WCS с Docker Hub

sudo docker pull flashphoner/webcallserver

Запустил контейнер WCS

sudo docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.1.10 \--net testnet --ip 192.168.1.10 \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Переменные здесь:

PASSWORD - пароль на доступ внутрь контейнера по SSH. Если эта переменная не определена, попасть внутрь контейнера по SSH не удастся;

LICENSE - номер лицензии WCS. Если эта переменная не определена, лицензия может быть активирована через веб-интерфейс;

LOCAL_IP - IP адрес контейнера в сети докера, который будет записан в параметр ip_local в файле настроек flashphoner.properties;

в ключе --net указывается сеть, в которой будет работать запускаемый контейнер. Запускаем контейнер в сети testnet.

Проверил доступность контейнера пингом:

ping 192.168.1.10

Открыл Web интерфейс WCS в локальном браузере по ссылке https://192.168.1.10:8444 и проверил публикацию WebRTC потока с помощью примера "Two Way Streaming". Все работает.

Локально, с моего компьютера на котором установлен Docker, доступ к WCS серверу у меня был. Теперь нужно было дать доступ коллегам.

Замкнутая сеть

Внутренняя сеть Docker является изолированной, т.е. из сети докера доступ "в мир" есть, а "из мира" сеть докера не доступна.

Получается, чтобы предоставить коллегам доступ к тестовому стенду в Docker на моей машине, я должен предоставить консольный доступ к своей машине. Для тестирования внутри группы разработчиков такой вариант с натяжкой допустим. Но мне то хотелось это все запустить в продакшен. Неужели, миллиарды контейнеров во всем мире работают только локально?

Конечно же нет. Путем курения мануалов был найден ответ. Нужно пробросить порты. Причем проброс портов нужен не на сетевом маршрутизаторе, а в самом Dockere.

Отлично! Список портов известен. Пробрасываем:

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.1.10 \-e EXTERNAL_IP=192.168.23.6 \-d -p8444:8444 -p8443:8443 -p1935:1935 -p30000-33000:30000-33000 \--net testnet --ip 192.168.1.10 \--name wcs-docker-test --rm flashphoner/webcallserver:latest

В этой команде используем следующие переменные:

PASSWORD, LICENSE и LOCAL_IP мы рассмотрели выше;

EXTERNAL_IP IP адрес внешнего сетевого интерфейса. Записывается в параметр ip в файле настроек flashphoner.properties;

Так же в команде появляются ключи -p это и есть проброс портов. В этой итерации используем ту же сеть "testnet", которую мы создали раньше.

В браузере на другом компьютере открываю https://192.168.23.6:8444 (IP адрес моей машины с Docker) и запускаю пример "Two Way Streaming"

Web интерфейс WCS работает и даже WebRTC трафик ходит.

И все было бы прекрасно, если бы не одно но!

Ну что ж так долго!

Контейнер с включенным пробросом портов запускался у меня около 10 минут. За это время я бы успел вручную поставить пару копий WCS. Такая задержка происходит из-за того, что Docker формирует привязку для каждого порта из диапазона.

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

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

Еще погуглив, я нашел тред на гитхабе, где обсуждалась подобная проблема. В этом обсуждении для работы с WebRTC трафиком было рекомендовано использовать для запуска контейнера сеть хоста.

Запускаем контейнер в сети хоста (на это указывает ключ --net host)

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.23.6 \-e EXTERNAL_IP=192.168.23.6 \--net host \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Отлично! Контейнер запустился быстро. С внешней машины все работает - и web интерфейс и WebRTC трафик публикуется и воспроизводится.

Потом я запустил еще пару контейнеров. Благо на моем компьютере несколько сетевых карт.

На этом можно было бы поставить точку. Но меня смутил тот факт, что количество контейнеров на хосте будет упираться в количество сетевых интерфейсов.

Рабочий вариант

Начиная с версии 1.12 Docker предоставляет два сетевых драйвера: Macvlan и IPvlan. Они позволяют назначать статические IP из сети LAN.

  • Macvlan позволяет одному физическому сетевому интерфейсу (машине-хосту) иметь произвольное количество контейнеров, каждый из которых имеет свой собственный MAC-адрес.

    Требуется ядро Linux v3.93.19 или 4.0+.

  • IPvlan позволяет создать произвольное количество контейнеров для вашей хост машины, которые имеют один и тот же MAC-адрес.

    Требуется ядро Linux v4.2 + (поддержка более ранних ядер существует, но глючит).

Я использовал в своей инсталляции драйвер IPvlan. Отчасти, так сложилось исторически, отчасти у меня был расчет на перевод инфраструктуры на VMWare ESXi. Дело в том, что для VMWare ESXi доступно использование только одного MAC-адреса на порт, и в таком случае технология Macvlan не подходит.

Итак. У меня есть сетевой интерфейс enp0s3, который получает IP адрес от DHCP сервера.

т.к. в моей сети адреса выдает DHCP сервер, а Docker выбирает и присваивает адреса самостоятельно, это может привести к конфликтам, если Docker выберет адрес, который уже был назначен другому хосту в сети.

Что бы этого избежать нужно зарезервировать часть диапазона подсети для использования Docker. Это решение состоит из двух частей:

  1. Нужно настроить службу DHCP в сети таким образом, чтобы она не назначала адреса в некотором определенном диапазоне.

  2. Нужно сообщить Docker об этом зарезервированном диапазоне адресов.

В этой статье я не буду рассказывать, как настраивать DHCP сервер. Думаю, каждый айтишник в своей практике сталкивался с этим не единожды, в крайнем случае, в сети полно мануалов.

А вот как сообщить Docker, какой диапазон для него выделен, разберем подробно.

Я ограничил диапазон адресов DHCP сервера так, что он не выдает адреса выше 192.168.23. 99. Отдадим для Docker 32 адреса начиная с 192.168.23.100.

Создаем новую Docker сеть с названием "new-testnet":

docker network create -d ipvlan -o parent=enp0s3 \--subnet 192.168.23.0/24 \--gateway 192.168.23.1 \--ip-range 192.168.23.100/27 \new-testnet

где:

ipvlan тип сетевого драйвера;

parent=enp0s3 физический сетевой интерфейс (enp0s3), через который будет идти трафик контейнеров;

--subnet подсеть;

--gateway шлюз по умолчанию для подсети;

--ip-range диапазон адресов в подсети, которые Docker может присваивать контейнерам.

и запускаем в этой сети контейнер с WCS

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.23.101 \-e EXTERNAL_IP=192.168.23.101 \--net new-testnet --ip 192.168.23.101 \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Проверяем работу web интерфейса и публикацию/воспроизведение WebRTC трафика с помощью примера "Two-way Streaming":

Есть один маленький минус такого подхода. При использовании технологий Ipvlan или Macvlan Docker изолирует контейнер от хоста. Если, например, попробовать пропинговать контейнер с хоста, то все пакеты будут потеряны.

Но для моей текущей задачи запуска WCS в контейнере это не критично. Всегда можно запустить пинг или подключиться по ssh с другой машины.

Используя технологию IPvlan на одном Docker хосте можно поднять необходимое количество контейнеров. Это количество ограничено только ресурсами хоста и, частично, сетевой адресацией конкретной сети.

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

Ссылки

WCS в Docker

Документация по развертыванию WCS в Docker

Образ WCS на DockerHub

Подробнее..

Тривиальная и неправильная облачная компиляция

28.01.2021 00:21:03 | Автор: admin


Введение


Данная статья не история успеха, а скорее руководство как не надо делать. Весной 2020 для поддержания спортивного тонуса участвовал в студенческом хакатоне (спойлер: заняли 2-е место). Удивительно, но задача из полуфинала оказалась более интересной и сложной чем финальная. Как вы поняли, о ней и своём решении расскажу под катом.


Задача


Данный кейс был предложен Deutsche Bank в направлении WEB-разработка.
Необходимо было разработать онлайн-редактор для проекта Алгосимулятор тестового стенда для проверки работы алгоритмов электронной торговли на языке Java. Каждый алгоритм реализуется в виде наследника класса AbstractTradingAlgorythm.


AbstractTradingAlgorythm.java
public abstract class AbstractTradingAlgorithm {    abstract void handleTicker(Ticker ticker) throws Exception;    public void receiveTick(String tick) throws Exception {        handleTicker(Ticker.parse(tick));    }    static class Ticker {        String pair;        double price;       static Ticker parse(String tick) {           Ticker ticker = new Ticker();           String[] tickerSplit = tick.split(",");           ticker.pair = tickerSplit[0];           ticker.price = Double.valueOf(tickerSplit[1]);           return ticker;       }    }}

Сам же редактор во время работы говорит тебе три вещи:


  1. Наследуешь ли ты правильный класс
  2. Будут ли ошибки на этапе компиляции
  3. Успешен ли тестовый прогон алгоритма. В данном случае подразумевается, что "В результате вызова new <ClassName>().receiveTick(RUBHGD,100.1) отсутствуют runtime exceptions".


Ну окей, скелет веб-сервиса через spring накидать дело на 5-10 минут. Пункт 1 работа для регулярных выражений, поэтому даже думать об этом сейчас не буду. Для пункта 2 можно конечно написать синтаксический анализатор, но зачем, когда это уже сделали за меня. Может и пункт 3 получится сделать, использовав наработки по пункту 2. В общем, дело за малым, уместить в один метод, ну например, компиляцию исходного кода программы на Java, переданного в контроллер строкой.


Решение


Здесь и начинается самое интересное. Забегая вперёд, как сделали другие ребята: установили на машину джаву, отдавали команды на ось и грепали stdout. Конечно, это более универсальный метод, но во-первых, нам сказали слово Java, а во-вторых...



у каждого свой путь.


Естественно, Java окружение устанавливать и настраивать всё же придётся. Правда компилировать и исполнять код мы будем не в терминале, а, как бы это ни звучало, в коде. Начиная с 6 версии, в Java SE присутствует пакет javax.tools, добавленный в стандартный API для компиляции исходного кода Java.
Теперь привычные понятия такие, как файлы с исходным кодом, параметры компилятора, каталоги с выходными файлами, сообщения компилятора, превратились в абстракции, используемые при работе с интерфейсом JavaCompiler, через реализации которого ведётся основная работа с задачами компиляции. Подробней о нём можно прочитать в официальной документации. Главное, что оттуда сейчас перейдёт моментально в текст статьи, это класс JavaSourceFromString. Дело в том, что, по умолчанию, исходный код загружается из файловой системы. В нашем же случае исходный код будет приходить строкой извне.


JavaSourceFromString.java
import javax.tools.SimpleJavaFileObject;import java.net.URI;public class JavaSourceFromString extends SimpleJavaFileObject {    final String code;    public JavaSourceFromString(String name, String code) {        super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);        this.code = code;    }    @Override    public CharSequence getCharContent(boolean ignoreEncodingErrors) {        return code;    }}

Далее, в принципе уже ничего сложного нет. Получаем строку, имя класса и преобразуем их в объект JavaFileObject. Этот объект передаём в компилятор, компилируем и собираем вывод, который и возвращаем на клиент.
Сделаем класс Validator, в котором инкапсулируем процесс компиляции и тестового прогона некоторого исходника.


public class Validator {    private JavaSourceFromString sourceObject;    public Validator(String className, String source) {        sourceObject = new JavaSourceFromString(className, source);    }}

Далее добавим компиляцию.


public class Validator {    ...    public List<Diagnostic<? extends JavaFileObject>> compile() {        // получаем компилятор, установленный в системе        var compiler = ToolProvider.getSystemJavaCompiler();        // компилируем        var compilationUnits = Collections.singletonList(sourceObject);        var diagnostics = new DiagnosticCollector<JavaFileObject>();        compiler.getTask(null, null, diagnostics, null, null, compilationUnits).call();        // возворащаем диагностику        return diagnostics.getDiagnostics();    }}

Пользоваться этим можно как-то так.


public void MyMethod() {        var className = "TradeAlgo";        var sourceString = "public class TradeAlgo extends AbstractTradingAlgorithm{\n" +                "@Override\n" +                "    void handleTicker(Ticker ticker) throws Exception {\n" +                "       System.out.println(\"TradeAlgo::handleTicker\");\n" +                "    }\n" +                "}\n";        var validator = new Validator(className, sourceString);        for (var message : validator.compile()) {            System.out.println(message);        }    }

При этом, если компиляция прошла успешно, то возвращённый методом compile список будет пуст. Что интересно? А вот что.

На приведённом изображении вы можете видеть директорию проекта после завершения выполнения программы, во время выполнения которой была осуществлена компиляция. Красным прямоугольником обведены .class файлы, сгенерированные компилятором. Куда их девать, и как это чистить, не знаю жду в комментариях. Но что это значит? Что скомпилированные классы присоединяются в runtime, и там их можно использовать. А значит, следующий пункт задачи решается тривиально с помощью средств рефлексии.
Создадим вспомогательный POJO для хранения результата прогона.


TestResult.java
public class TestResult {    private boolean success;    private String comment;    public TestResult(boolean success, String comment) {        this.success = success;        this.comment = comment;    }    public boolean success() {        return success;    }    public String getComment() {        return comment;    }}

Теперь модифицируем класс Validator с учётом новых обстоятельств.


public class Validator {    ...    private String className;    private boolean compiled = false;    public Validator(String className, String source) {        this.className = className;        ...    }    ...    public TestResult testRun(String arg) {        var result = new TestResult(false, "Failed to compile");        if (compiled) {            try {                // загружаем класс                var classLoader = URLClassLoader.newInstance(new URL[]{new File("").toURI().toURL()});                var c = Class.forName(className, true, classLoader);                // создаём объект класса                var constructor = c.getConstructor();                var instance = constructor.newInstance();                // выполняем целевой метод                c.getDeclaredMethod("receiveTick", String.class).invoke(instance, arg);                result = new TestResult(true, "Success");            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException | RuntimeException | MalformedURLException | InstantiationException e) {                var sw = new StringWriter();                e.printStackTrace(new PrintWriter(sw));                result = new TestResult(false, sw.toString());            }        }        return result;    }}

Возвращаясь к предыдущему примеру использования, можно дописать туда такие строчки.


public void MyMethod() {        ...        var result = validator.testRun("RUBHGD,100.1");        System.out.println(result.success() + " " + result.getComment());    }

Вставить этот код в реализацию API контроллера задача нетрудная, поэтому подробности её решения можно опустить.


Какие проблемы?


  1. Ещё раз напомню про кучу .class файлов.


  2. Поскольку опять же идёт работа с компиляцией некоторых классов, есть риск отказа в записи любого непредвиденного .class файла.


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



Поэтому делать в точности как я не надо)


P.S. Ссылка на гитхаб с исходным кодом из статьи.

Подробнее..

Развертывание ML модели в Docker с использованием Flask (REST API) масштабирование нагрузки через Nginx балансер

27.03.2021 18:23:22 | Автор: admin

Как известно настройка и обучение моделей машинного обучения это только одна из частей цикла разработки, не менее важной частью является развертывание модели для её дальнейшего использования. В этой статье я расскажу о том, как модель машинного обучения может быть развернута в виде Docker микросервиса, а также о том, как можно распараллелить работу микросервиса с помощью распределения нагрузки в несколько потоков через Load balancer. В последнее время Docker набрал большую популярность, однако здесь будет описан только один из видов стратегий развертывания моделей, и в каждом конкретном случае выбор лучшего варианта остаётся за разработчиком.



Гитхаб репозиторий с исходным кодом: https://github.com/cdies/ML_microservice


Введение


Для этого примера я использовал распространённый набор данных MNIST. Конечная ML модель будет развернута в Docker контейнере, доступ к которой будет организован через HTTP протокол посредствам POST запроса (архитектурный стиль REST API). Полученный таким образом микросервис будет распараллелен через балансировщик на базе Nginx.


Веб фреймворк Flask уже содержит в себе веб-сервер, однако, он используется строго for dev purpose only, т.е. только для разработки, вследствие этого я воспользовался веб-сервером Gunicorn для предоставления нашего REST API.


Описание ML модели


Как уже было отмечено выше, для построения ML модели я использовал, наверное, один из самых известных наборов данных MNIST, тут в принципе показан стандартный пайплайн: загрузка и обработка данных -> обучение модели на нейронной сети Keras -> сохранение модели для повторного использования. Исходный код в файле mnist.py


from tensorflow.keras.layers import Dense, Conv2D, MaxPooling2D, Flatten, Dropoutfrom tensorflow.keras.models import Sequentialfrom tensorflow import kerasfrom tensorflow.keras.datasets import mnist(x_train, y_train), (x_test, y_test) = mnist.load_data()x_train = x_train.reshape((60000,28,28,1)).astype('float32')/255x_test = x_test.reshape((10000,28,28,1)).astype('float32')/255y_train = keras.utils.to_categorical(y_train, 10)y_test = keras.utils.to_categorical(y_test, 10)model = Sequential()model.add(Conv2D(32, (3,3), activation='relu', input_shape=(28,28,1)))model.add(Conv2D(64, (3,3), activation='relu'))model.add(MaxPooling2D(pool_size=(2,2)))model.add(Dropout(0.25))model.add(Flatten())model.add(Dense(128, activation='relu'))model.add(Dropout(0.5))model.add(Dense(10, activation='softmax'))model.compile(loss=keras.losses.categorical_crossentropy,     optimizer=keras.optimizers.Adadelta(), metrics=['accuracy'])model.fit(x_train, y_train, batch_size=128,     epochs=12, verbose=1, validation_data=(x_test, y_test))score = model.evaluate(x_test, y_test)print(score)# Save modelmodel.save('mnist-microservice/model.h5')

Построение HTTP REST API


Сохранённую ML модель я буду использовать для создания простого REST API микросервиса, для этого воспользуюсь веб-фреймворком Flask. Микросервис будет принимать изображение цифры, приводить его к виду, подходящему для использования в нейронной сети, которую мы сохранили в файле model.h5 и возвращать распознанное значение и его вероятность. Исходный код в файле mnist_recognizer.py


from flask import Flask, jsonify, requestfrom tensorflow import kerasimport numpy as npfrom flask_cors import CORSimport imageapp = Flask(__name__)# Cross Origin Resource Sharing (CORS) handlingCORS(app, resources={'/image': {"origins": "http://localhost:8080"}})@app.route('/image', methods=['POST'])def image_post_request():      model = keras.models.load_model('model.h5')    x = image.convert(request.json['image'])    y = model.predict(x.reshape((1,28,28,1))).reshape((10,))    n = int(np.argmax(y, axis=0))    y = [float(i) for i in y]    return jsonify({'result':y, 'digit':n})if __name__ == "__main__":    app.run(host='0.0.0.0', port=5000)

Docker файл микросервиса


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


FROM python:3.7RUN python -m pip install flask flask-cors gunicorn numpy tensorflow pillowWORKDIR /appADD image.py image.pyADD mnist_recognizer.py mnist_recognizer.pyADD model.h5 model.h5EXPOSE 5000CMD [ "gunicorn", "--bind", "0.0.0.0:5000", "mnist_recognizer:app" ]

В таком виде микросервис уже можно использовать в однопоточном режиме, для этого нужно выполнить следующие команды в папке mnist-microservice:


docker build -t mnist_microservice_test .

docker run -d -p 5000:5000 mnist_microservice_test

Nginx балансер


Тут сразу стоит уточнить, что, в принципе распараллелить процесс можно было бы с помощью Gunicorn веб-сервера, в частности добавить в Dockerfile в строку запуска Gunicorn веб-сервера --workers n, чтобы было n процессов. Однако я исходил из того, что в Docker контейнере не нужно плодить процессы, кроме необходимых, поэтому решил разделить процессы по контейнерам (одна ML модель один контейнер), а не сваливать все процессы в один контейнер. (Пишите в комментах, как бы сделали вы)


Чтобы балансировщик нагрузки работал, нужно, чтобы Nginx перенаправлял запросы на порт 5000, который слушает наш микросервис. Исходный код в файле nginx.conf


user  nginx;events {    worker_connections   1000;}http {        server {              listen 4000;              location / {                proxy_pass http://mnist-microservice:5000;              }        }}

В заключительном docker-compose.yml файле я распараллелил созданный ранее микросервис, который назвал здесь mnist-microservice с помощью параметра replicas.


version: '3.7'services:    mnist-microservice:        build:            context: ./mnist-microservice        image: mnist-microservice        restart: unless-stopped        expose:            - "5000"        deploy:            replicas: 3    nginx-balancer:        image: nginx        container_name: nginx-balancer        restart: unless-stopped        volumes:            - ./nginx-balancer/nginx.conf:/etc/nginx/nginx.conf:ro        depends_on:            - mnist-microservice        ports:            - "5000:4000"    nginx-html:        image: nginx        container_name: nginx-html        restart: unless-stopped        volumes:            - ./html:/usr/share/nginx/html:ro        depends_on:            - nginx-balancer        ports:            - "8080:80"

Как видно, микросервис также продолжает слушать порт 5000 внутри виртуальной сети докера, в то же самое время nginx-balancer перенаправляет трафик от порта 4000 к порту 5000 также внутри виртуальной сети докера, а уже порт 5000 внешней сети я пробросил на 4000 внутренний порт nginx-balancer. Таким образом, для внешнего веб-сервера nginx-html ничего не поменялось, также не пришлось менять исходный код микросервиса.



Чтобы всё запустить, необходимо выполнить в корневой папке проекта:


docker-compose up --build

Для проверки работы микросервиса можно открыть адрес http://localhost:8080/ в браузере и начать посылать в него цифры нарисованные мышкой, в результате должно получиться что-то вроде этого:



Выводы


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


Полезные ссылки:
https://medium.com/swlh/machine-learning-model-deployment-in-docker-using-flask-d77f6cb551d6
https://medium.com/@vinodkrane/microservices-scaling-and-load-balancing-using-docker-compose-78bf8dc04da9
https://github.com/deadfrominside/keras-flask-app

Подробнее..

Из песочницы Что такое REST API

14.08.2020 18:11:43 | Автор: admin
Если говорить отдаленно, то REST API нужен для создания сайта. Но ведь для этого много чего нужно (скажете Вы) в какой же части REST API?

Frontend и Backend


Начнем по порядку. Что такое frontend и backend? У сайта есть две стороны: лицевая и внутренняя соответственно. Первая обычно отвечает за визуальное расположение объектов на странице (где какие картинки, где какой текст и где какие кнопочки). Вторая отвечает за действия. Обычно это нажатия на те самые кнопочки или на другие штуки на сайте. Например, Вы заходите на страницу вашей любимой социальной сети и для начала Вам нужно войти в аккаунт. С лицевой стороны (frontend) Вы вводите логин и пароль и нажимаете кнопку Войти. В это время запрос отправляется в базу данных для проверки наличия такого пользователя, и в случае успеха, Вы попадаете в социальную сеть под своим аккаунтом, а в противном случае, Вы увидите сообщение об ошибке. В данном случае, за отправку запроса в базу данных по сути и отвечает backend сторона. Обычно ее разделяют на три подчасти:

  1. Web API для приема запросов
  2. Бизнес-логика для обработки запросов
  3. Взаимодействие с базой данных

В этой статье мы поговорим в основном про API или Application Programming Interface и немного про бизнес-логику. Но для начала создадим сервер.

Создание собственного сервера


Так выглядит простейшее серверное приложение на python с использованием фреймворка flask:

from flask import Flaskapp = Flask(__name__)@app.route("/")def index():   return "Hello, World!"app.run()

Здесь уже есть один пустой роут (/) и если запустить это приложение и открыть браузер на странице 127.0.0.1:5000, то Вы увидете надпись Hello, World!. Со стороны сервера, Вы увидите такое сообщение:

127.0.0.1 - [07/Aug/2020 20:32:16] GET / HTTP/1.1 200 Таким образом, переходя в браузере (клиенте) по данной ссылке, мы делаем GET-запрос на наш сервер и попадаем в функцию index отсюда и берется Hello, World!. Можно добавлять и другие запросы (гораздо более сложные) по другим роутам (или необязательно). Как я сказал, в данном случае у нас использовался GET-запрос стандартный по умолчанию. Но существует и множество других, самые популярные из которых POST, PUT, DELETE. Но зачем это нужно?

Create Read Update Delete


Во-первых, REST расшифровывается как REpresentational State Transfer (или, по-простому, РЕпрезентативная передача состояния). По факту само определение REST не так важно, но его обычно связывают с другой аббревиатурой CRUD Create Read Update Delete. В самом начале я приводил пример, связанный с базой данных и эти четыре операции неотъемлемая часть работы с ней (ну или просто с данными).

Во-вторых, REST или RESTfull API должны поддерживать обработку этих четырех действий. Здесь нам как раз и пригодятся методы GET, POST, PUT, DELETE. Как правило (не обязательно!) метод POST используется для добавления новых данных (Create), GET для чтения (Read), PUT для обновления существующих данных (Update) и DELETE соответственно для удаления (Delete). Например, то же самое приложение на flask можно переделать так:

from flask import Flask, requestapp = Flask(__name__)@app.route("/", methods=["POST", "GET", "PUT", "DELETE"])def index():   if request.method == "POST":       # добавить новые данные   if request.method == "GET":       # отдать данные   if request.method == "PUT":       # обновить данные   if request.method == "DELETE":       # удалить данныеapp.run()

Это и есть примитивный REST API. Frontend сторона теперь может посылать запросы и, в зависимости от их типа, мы будем производить дальнейшие действия.

Работа с данными


Наше текущее приложение совсем неинтересное хорошо бы поработать с какими-нибудь данными. Для этого надо подумать, как их передавать. Самый популярный способ JSON-формат (но можно использовать и другие, например, XML). Он представляет из себя аналог словаря в python и очень удобен в использовании. Я буду использовать примитивные данные для примера с авторизацией в социальной сети:

data = {   1: {       "login": "login1",       "password": "Qwerty1"},   2: {       "login": "login2",       "password": "Ytrewq2"}   }

У нас есть data, в которой пока что два пользователя (login1 и login2) и мы будем эту дату CRUDить. Стоит сказать, что все четыре метода редко когда работают на одном и том же роуте и обычно делают так: для методов GET (выдать всех пользователей) и POST используется роут, например, /users, а для методов GET (выдать одного пользователя по его id), PUT и DELETE /users/id. Также необходимо заметить, что для обновления и создания новых пользователей к нам приходят данные о них в теле запроса (request.json). Теперь нашу программу можно переписать следующим образом:

from flask import Flask, requestapp = Flask(__name__)data = {   1: {       "login": "login1",       "password": "Qwerty1"},   2: {       "login": "login2",       "password": "Ytrewq2"}   }@app.route("/users", methods=["POST", "GET"])def work_with_users():   if request.method == "POST":       data[max(data.keys())+1] = request.json       return {"message": "User was created"}, 201   if request.method == "GET":       return data, 200@app.route("/users/<int:user_id>", methods=["GET", "PUT", "DELETE"])def work_with_user_by_id(user_id):   if request.method == "GET":       return data[user_id], 200   if request.method == "PUT":       data[user_id]["login"] = request.json["login"]       data[user_id]["password"] = request.json["password"]       return {"message": "User was updated"}, 200   if request.method == "DELETE":       data.pop(user_id)       return {"message": "User was deleted"}, 200app.run()

Для тестирования запросов существует множество программ (Postman, Fiddler, Insomnia...) и я рекомендую ознакомиться с одной из них (лично мне больше всего нравится Postman). С их помощью можно посмотреть, что приходит в результате запроса и с каким статус-кодом (числа 200/201 в returnах). А также можно инсценировать отправку данных, добавляя их в тело запроса.

Стоит также заметить, что в настоящее время такой подход не используется, а обычно применяют библиотеку flask-restplus (или пришедшую ей на смену flask-restx), но я считаю, что для начала нужно познакомиться с чистым flask. Также необходимо проверять наличие данных и их корректность и предусмотреть возврат ошибки в противных случаях.

Заключение


REST API это просто методы CRUD, к которым обращается клиентская сторона сайта по определенным роутам. На слух и взгляд, возможно, это воспринимается трудно, так что я рекомендую написать собственный сервер по аналогии с примером. Лично я считаю flask одним из самых простых фреймворков для этого, и, если Вы новичок, то я советую попробовать именно его.
Подробнее..
Категории: Python , Api , Rest , Rest api , Flask , Crud restful api

Как быстро получить много данных от Битрикс24 через REST API

17.01.2021 12:19:01 | Автор: admin

Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционный способ для этого - обращение к серверу через метод *.list (например, crm.lead.list для лидов) с параметром select, перечисляющим список требуемых полей.

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

Стратегии

Ниже мы описываем три стратегии, которые мы условно назвали "ID filter", "Start increment' и "List + get".

Первые две стратегии ("ID filter" и "Start increment") предложены в официальной документации Битрикс24, но мы ниже предлагаем их "докрутить".

ID filter

Запросы отправляются к серверу последовательно с параметром "order": {"ID": "ASC"} (сортировка по возрастанию ID), и в каждом последующем запросе используются результаты предыдущего (фильтрация по ID, где ID > максимального ID в результатах предыдущего запроса).

При этом для ускорения используется параметр start = -1 для отключения затратной по времени операции расчета общего количества записей (поле total), которое по умолчанию возвращается в каждом ответе сервера при вызове методов вида *.list.

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

Start increment

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

Если перед нами стоит цель получить полный набор сущностей, то, в отличие от предыдущей стратегии, эта хорошо поддается ускорению (несмотря на то, что она требует использования параметра start, что замедляет работу сервера).

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

Объединение запросов в батчи

Зная общее количество элементов, можно сразу создать запросы на все страницы и объединить их в батчи. (Битрикс поддерживает батчи в 50 запросов.) Использование батчей позволяет обойти ограничения на максимальную скорость запросов, так как один батч считается за один запрос при учете сервером количества получаемых запросов.

Параллельная отправка батчей к серверу

Примеры кода в официальной документации Битрикс24 REST API везде предлагают последовательную отправку запросов и описывают лишь ограничения на скорость отправки запросов. Но параллельная отправка запросов возможна и позволяет сильно ускорить обмен информацией с сервером.

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

Именно такая стратегия сейчас заложена в метод get_all() в питоновской библиотеке fast_bitrix24 (пиарюсь - библиотеку написал я).

List + get

Составная стратегия, при которой при помощи стратегии "Start increment" от сервера получается сначала список всех ID по методу *.list (с указанием, что нужны только ID - 'select': ['ID']) , а потом через метод *.get получается содержимое всех полей для каждого ID. При этом в обоих шагах используются описанные выше способы ускорения "Объединение запросов в батчи" и "Параллельная отправка батчей".

Тест

Чтобы проверить эффективность этих стратегий, мы провели тест (код теста).

Тест запрашивает страницы лидов (метод crm.lead.list) через 3 вышеописанные стратегии (при этом стратегия "ID filter" реализована в один поток - с начала списка ID). Для каждой стратегии запрашиваются 1, 50, 100 и 200 страниц и замеряется время выполнения запроса.

Тест использует библиотеку fast_bitrix24 для автоматического контроля скорости запросов к серверу Битрикс24.

Тест проводим на 7-й версии REST API на списке в ~35000 лидов.

Результаты теста

Getting 1 pages:ID filter: 0.3 sec.Start increment: 0.73 sec.Getting ID list for the 'list+get' strategy, method crm.lead: 2.17 sec.List + get: 2.61 sec.Getting 50 pages:ID filter: 12.8 sec.Start increment: 21.39 sec.List + get: 1.84 sec.Getting 100 pages:ID filter: 49.67 sec.Start increment: 39.97 sec.List + get: 3.28 sec.Getting 200 pages:ID filter: 99.67 sec.Start increment: 78.05 sec.List + get: 6.36 sec.

Выводы

В целом, стратегии, использующие батчи и параллельные запросы ("Start increment" и "List + get"), показали себя лучше.

Однако при этом, к моему удивлению, стратегия "List + get" оказалась на порядок продуктивнее остальных, даже несмотря на то, что в ней приходится пробегаться по всему списку два раза. (Возможно, эту статью увидят разработчики Битрикс24 и объяснят этот феномен?)

Я не уверен в существовании высокоуровневых библиотек для PHP, позволяющих пользователю реализовывать такие стратегии, не парясь упаковкой запросов в батчи и организацией параллельных запросов с контролем их скорости. Но если вы пишете на Python - милости прошу использовать fast_bitrix24, который позволяет выгружать данные из Битрикс24 со скоростью до тысяч элементов в секунду.

Подробнее..

Как написать удобный API 10 рекомендаций

25.05.2021 14:04:29 | Автор: admin

Я разработчик и большую часть моей карьеры я строю API различных сервисов. Рекомендации для этой статьи, были собраны на основе наиболее часто встречающихся проблем при проектировании своего сервиса в команде или использовании сторонних API.

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

1. Не используйте глаголы в URL *

* - если это одна из CRUD-операций.

За действие с ресурсом отвечают CRUD-методы запроса: POST - создать (create), GET - получить (read), PUT/PATH - обновить (update), DELETE - удалить (ну вы поняли). Плохо:

POST /users/{userId}/delete - удаление пользователяPOST /bookings/{bookingId}/update - обновление бронировки

Хорошо:

DELETE /users/{userId}PUT /bookings/{bookingId}

2. Используйте глаголы в URL

Плохо:

POST /users/{userId}/books/{bookId}/create - добавить книгу пользователю

Хорошо:

POST /users/{userId}/books/{bookId}/attachPOST /users/{userId}/notifications/send - отправить уведомление пользователю

3. Выделяйте новые сущности

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

POST /wishlist/{userId}/{bookId}

4. Используйте один идентификатор ресурса *

* - если ваша структура данных это позволяет.

Это значит если у вас есть записи вида один ко многим, например
бронь -> путешественники (booking->travellers), вам будет достаточно передавать в запросе идентификатор путешественника.

Плохо:

# получение данных путешественникаGET /bookings/{bookingId}/travellers/{travellerId}

Хорошо:

GET /bookings/travellers/{travellerId}

Так же замечу что /bookings/travellers/ лучше чем просто /travellers хорошо придерживаться иерархии данных в своем API.

5. Все ресурсы во множественном числе

Плохо:

GET /user/{userId} - получение данных пользователяPOST /ticket/{ticketId}/book - бронирование билета

Хорошо:

GET /users/{userId}POST /tickets/{ticketId}/book

6. Используйте HTTP-статусы по максимуму

Самый простой способ обработки ошибок - это ответить соответствующим кодом состояния. В большинстве случает один этот статус может дать исчерпывающую информацию о результате обработки запроса. Одни из самых распространенных кодов ответов:

  • 400 Bad Request - клиент отправил неверный запрос, например, отсутствует обязательный параметр запроса.

  • 401 Unauthorized - клиенту не удалось пройти обязательную аутентификацию на сервере для обработки запроса.

  • 403 Forbidden - клиент аутентифицирован, но не имеет разрешения на доступ к запрошенному ресурсу.

  • 404 Not Found - запрошенный ресурс не существует.

  • 409 Conflict - этот ответ отправляется, когда запрос конфликтует с текущим состоянием сервера.

  • 500 Internal Server Error - на сервере произошла общая ошибка.

  • 503 Service Unavailable - запрошенная услуга недоступна.

7. Модификаторы получения ресурса

Логика построения роутов может быть не связана с архитектурой проекта или структурой базы данных. Например, в бд есть викторины и пройденные викторины - две отдельные таблицы (quizzes и passed_quizzes). Но для апи это могут быть просто викторины, а пройденные викторины это модификатор.

Пример: /quizzes и /quizzes/passed. Здесь quizzes - ресурс (викторины), passed - модификатор (пройденные).

Плохо:

GET /passed-quizzes - получение пройденных викторинGET /booked-tickets - получение забронированных билетовPOST /gold-users - создание премиум пользователя

Хорошо:

GET /tickets/bookedPOST /users/gold

8. Выберите одну структуру ответов

Когда на два запроса к API может быть получен совсем разный по структуре ответ - это грустно. Старайтесь сформировать одну четкую структуру которой всегда будете придерживаться, будет круто еще включить служебные поля, несущие дополнительную информацию.

Плохо:

GET /book/{bookId}{    "name": "Harry Potter and the Philosopher's Stone",    "genre": "fantasy",    "status": 0, # статус вашего приложения    "error": false,     ...}

Хорошо:

GET /book/{bookId}{    "status": 0,    "message": "ok",    "data": {...}}

В этом примере 3 поля универсальны и могут использоваться для любого ответа от апи. status, message - собственный статус и сообщение приложения по которому клиент сможет ориентироваться, эти поля сообщат ему дополнительную информацию о процессе обработки запроса, но не данные ресурса. Например, в нашем приложении в один момент времени, пользователь может проходить только одну викторину. Тогда запрос на начало новой может выдать 409й статус а в полях status и message дополнительную информацию почему была получена ошибка.

9. Все параметры и json в camelCase

9.1 В параметрах запросов
Плохо:

GET /users/{user-id}GET /users/{user_id}GET /users/{userid}

Хорошо:

GET /users/{userId}POST /ticket/{ticketId}/gold

9.2 В теле ответа или принимаемого запроса
Плохо:

{    "ID": "fb6ad842-bd8d-47dd-b7e1-68891d8abeec",    "Name": "soccer3000",    "provider_id": 1455,    "Created_At": "25.05.2020"}

Хорошо:

{    "id": "fb6ad842-bd8d-47dd-b7e1-68891d8abeec",    "Name": "soccer3000",    "providerId": 1455,    "createdAt": "25.05.2020"}

10. Пользуйтесь Content-Type

Плохо:

GET /tickets.jsonGET /tickets.xml

Хорошо:

GET /tickets// и в хедереСontent-Type: application/json// илиСontent-Type: application/xml

Заключение

Перечисленные выше рекомендации это далеко не весь список способов сделать API лучше. Для дальнейшего изучения рекомендую разобрать спецификации REST API и список кодов http-статусов (вы удивитесь на сколько их много и какие ситуации они охватывают).

А в комментариях, предлагаю написать свою рекомендацию по построению REST API которую вы считаете важной.

Подробнее..

Как создать простое Rest API на .NET Core

03.12.2020 12:07:50 | Автор: admin

Введение

Всем привет, в данной статье будет рассказано, как с использованием технологии C# ASP.NET Core написать простое Rest Api. Сделать Unit-тесты на слои приложений. Отправлять Json ответы. Также покажу, как выложить данное приложение в Docker.

В данной статье не будет описано, как делать клиентскую (далее Front) часть приложения. Здесь я покажу только серверную (далее Back).

Что используем?

Писать код я буду в Visual Studio 2019.

Для реализации приложения, я буду использовать такие библиотеки NuGet:

  1. Microsoft.EntityFrameworkCore

  2. Microsoft.EntityFrameworkCore.SqlServer

  3. Microsoft.EntityFrameworkCore.Tools

Для тестов вот эти библиотеки:

  1. Microsoft.NET.Test.Sdk

  2. Microsoft.NETCore.App

  3. Moq

  4. xunit

  5. xunit.runner.visualstudio

Для установки пакетов нужно зайти в обозреватель пакетов NuGet, сделать это можно, нажав ПКМ по проекту, и выбрав там пункт управление пакетам NuGet

Что программировать?

Для примера я возьму сильно упрощенную модель сервиса по ремонту автомобилей. В моей модели будут работники, которые будут заниматься ремонтом, автомобили, поступающие на ремонт, и документация по ремонту, которая будет отсылаться в ответе.

Настройка Базы Данных

Для настройки базы данных нужен класс ApplicationContext (реализация будет далее) и строка подключения, которая храниться в файле appsettings.json. В этом классе будут прописаны все зависимости для генерации миграций. Строка подключения нужна для того, чтобы приложение знало в какую БД ей обращаться и с какими параметрами.

Чтобы добавить строку подключения, достаточно зайти в файл appsettings.json и прописать следующие строки:

"ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=testdb;Trusted_Connection=True;" },

Описание слоев приложения

Модели

В слое моделей будут находиться сущности, которые с помощью Entity Framework будут преобразованы в таблицы в базе данных.

Для описания модели в приложении достаточно просто описать класс, с нужными вам полями. Эти поля автоматически будут преобразованы в столбцы таблицы, а название таблицы будет соответствовать названию класса. Так задано по умолчанию, но есть специальные атрибуты, которые позволяют более гибко настраивать хранение данных в БД (но о них не в этой статье).

Первая модель, которая понадобиться для описания сервиса по ремонту - модель сотрудника. Что она будет из себя представлять?

  • Уникальный идентификатор сотрудника

  • Имя сотрудника

  • Должность сотрудника

  • Номер телефона для связи с сотрудником

Следующая модель для описания сервиса - автомобили, которые будут поступать на ремонт.

  • Уникальный идентификатор автомобиля

  • Название автомобиля

  • Номер автомобиля

И последняя модель, которую мы уже будем отсылать - документ (выписка) по ремонту.

  • Уникальный идентификатор документа

  • Сотрудник, который обслуживал автомобиль

  • Автомобиль, который был на ремонте

Чтобы модели попали в базу данных, необходимо создать миграцию. Миграция - описание того, как и что будет записано в базу данных. С помощью Entity Framework миграции можно генерировать автоматически. Для этого в пакетном менеджере надо прописать команду "Add-Migration". После этого Entity Framework сгенерирует миграцию по вашим моделям, которые указаны в классе DbContext. Чтобы применить миграцию, используем команду "Update-Database", после этого ваши данные попадут в базу данных (как это применять будет описано далее).

Контроллеры

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

Для возвращаемого значения в контроллерах будут использоваться тип Json. Для этого достаточно в return прописать

new JsonResult(Ваш объект)

В данном примере, я покажу как сделать методы для GET, POST, PUT и DELETE запросов. В GET-запросе я буду выбирать все существующие документы и передавать их на Front, а в POST-запросе я буду вызывать сервис по ремонту автомобиля и возвращать выписку по ремонту, PUT будет отвечать за обновление существующего документа и DELETE за удаление документа.

DAO (Репозитории)

Репозитории нужны как посредники для обеспечения работы с БД, чтобы исключить прямое взаимодействие человека с данными. Это нужно для того, чтобы сокрыть логику работы автоматизировать многие моменты работы с БД, а также для безопасной работы с данными.

В своем приложении я сделал репозиторий, который может принимать любую модель, и выполнять такие действия как get, get all, update, create, delete.

Сервисы

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

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

Реализация

Теперь, когда описано что и как будет устроено в приложении можно приступить и к реализации.

Создание проекта

При создании нового проекта, я выбрал веб-приложение ASP.NET Core, далее прописал его название (RestApi) и выбрал папку, где оно будет храниться. На экране выбора шаблона выбрал API.

Выбор шаблона приложенияВыбор шаблона приложения

Далее приступим к самому приложению.

Структура

Я разделил все приложение по папкам (также Unit-тесты в отдельном проекте) и получил вот такую структуру мое приложения:

Структура приложенияСтруктура приложения

Модели

Для реализации моделей я сделал абстрактный класс BaseModel. Он понадобиться в будущем для корректного наследования, а также в нем прописан Id каждой, модели (это помогает не дублировать код):

 public abstract class BaseModel { public Guid Id { get; set; } }

Далее вышеописанные модели:

 public class Car : BaseModel { public string Name { get; set; } public string Number { get; set; } }
 public class Document : BaseModel { public Guid CarId { get; set; } public Guid WorkerId { get; set; } public virtual Car Car { get; set; } public virtual Worker Worker { get; set; } }
 public class Worker : BaseModel { public string Name { get; set; } public string Position { get; set; } public string Telephone { get; set; } }

Репозиторий

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

Интерфейс:

public interface IBaseRepository<TDbModel> where TDbModel : BaseModel    {        public List<TDbModel> GetAll();        public TDbModel Get(Guid id);        public TDbModel Create(TDbModel model);        public TDbModel Update(TDbModel model);        public void Delete(Guid id);    }

Реализация:

    public class BaseRepository<TDbModel> : IBaseRepository<TDbModel> where TDbModel : BaseModel    {        private ApplicationContext Context { get; set; }        public BaseRepository(ApplicationContext context)        {            Context = context;        }        public TDbModel Create(TDbModel model)        {            Context.Set<TDbModel>().Add(model);            Context.SaveChanges();            return model;        }        public void Delete(Guid id)        {            var toDelete = Context.Set<TDbModel>().FirstOrDefault(m => m.Id == id);            Context.Set<TDbModel>().Remove(toDelete);            Context.SaveChanges();        }        public List<TDbModel> GetAll()        {            return Context.Set<TDbModel>().ToList();        }        public TDbModel Update(TDbModel model)        {            var toUpdate = Context.Set<TDbModel>().FirstOrDefault(m => m.Id == model.Id);            if (toUpdate != null)            {                toUpdate = model;            }            Context.Update(toUpdate);            Context.SaveChanges();            return toUpdate;        }        public TDbModel Get(Guid id)        {            return Context.Set<TDbModel>().FirstOrDefault(m => m.Id == id);        }    }

Сервис

Сервис также как и репозиторий имеет интерфейс и его реализацию.

Интерфейс:

public interface IRepairService    {        public void Work();    }

Реализация:

public class RepairService : IRepairService    {        private IBaseRepository<Document> Documents { get; set; }        private IBaseRepository<Car> Cars { get; set; }        private IBaseRepository<Worker> Workers { get; set; }        public void Work()        {            var rand = new Random();            var carId = Guid.NewGuid();            var workerId = Guid.NewGuid();            Cars.Create(new Car            {                Id = carId,                Name = String.Format($"Car{rand.Next()}"),                Number = String.Format($"{rand.Next()}")            });            Workers.Create(new Worker            {                Id = workerId,                Name = String.Format($"Worker{rand.Next()}"),                Position = String.Format($"Position{rand.Next()}"),                Telephone = String.Format($"8916{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}{rand.Next()}")            });            var car = Cars.Get(carId);            var worker = Workers.Get(workerId);            Documents.Create(new Document {                CarId = car.Id,                WorkerId = worker.Id,                Car = car,                Worker = worker            });        }    }

Контроллер

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

ДоменноеИмя/НазваниеКонтроллера/НазваниеМетода?Параметры(если есть)

Пути гибко настраиваются с помощью специальных атрибутов (о них не в этой статье).

Мой MainController:

[ApiController]    [Route("[controller]")]    public class MainController : ControllerBase    {        private IRepairService RepairService { get; set; }        private IBaseRepository<Document> Documents { get; set; }        public MainController(IRepairService repairService, IBaseRepository<Document> document )        {            RepairService = repairService;            Documents = document;        }        [HttpGet]        public JsonResult Get()        {            return new JsonResult(Documents.GetAll());        }        [HttpPost]        public JsonResult Post()        {            RepairService.Work();            return new JsonResult("Work was successfully done");        }        [HttpPut]        public JsonResult Put(Document doc)        {            bool success = true;            var document = Documents.Get(doc.Id);            try            {                if (document != null)                {                    document = Documents.Update(doc);                }                else                {                    success = false;                }            }            catch (Exception)            {                success = false;            }            return success ? new JsonResult($"Update successful {document.Id}") : new JsonResult("Update was not successful");        }        [HttpDelete]        public JsonResult Delete(Guid id)        {            bool success = true;            var document = Documents.Get(id);            try            {                if (document != null)                {                    Documents.Delete(document.Id);                }                else                {                    success = false;                }            }            catch (Exception)            {                success = false;            }            return success ? new JsonResult("Delete successful") : new JsonResult("Delete was not successful");        }    }

Application Context

ApplicationContext класс, который унаследован от класса DbContext. В нем прописываются все DbSet. С их помощью приложение знает, какие модели должны быть в базе данных, а какие нет.

public class ApplicationContext: DbContext    {        public DbSet<Car> Cars { get; set; }        public DbSet<Document> Documents { get; set; }        public DbSet<Worker> Workers { get; set; }        public ApplicationContext(DbContextOptions<ApplicationContext> options): base(options)        {            Database.EnsureCreated();        }    }

Настройка зависимостей и инжектирования

А теперь немного про инжектирование. Правильная настройка зависимостей проекта Asp.net core позволяет упростить его работу и избежать лишнего написания кода. Все зависимости прописываются в файле Startup.cs.

Что я связывал? Я связывал интерфейс репозитория с репозиторием каждой модели (далее будет видно, что имеется ввиду), также я связал интерфейс сервиса с его реализацией.

Также в этом же файле прописываются настройки для базы данных. Помните про строку подключения из начала статьи? Так вот сейчас мы ее и используем для настройки БД.

Вот как выглядит мой файл Startup.cs:

public Startup(IConfiguration configuration)        {            Configuration = configuration;        }        public IConfiguration Configuration { get; }        // This method gets called by the runtime. Use this method to add services to the container.        public void ConfigureServices(IServiceCollection services)        {            string connection = Configuration.GetConnectionString("DefaultConnection");            services.AddMvc();            services.AddDbContext<ApplicationContext>(options =>                options.UseSqlServer(connection));            services.AddTransient<IRepairService, RepairService>();            services.AddTransient<IBaseRepository<Document>, BaseRepository<Document>>();            services.AddTransient<IBaseRepository<Car>, BaseRepository<Car>>();            services.AddTransient<IBaseRepository<Worker>, BaseRepository<Worker>>();        }        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)        {            if (env.IsDevelopment())            {                app.UseDeveloperExceptionPage();            }            app.UseHttpsRedirection();            app.UseRouting();            app.UseAuthorization();            app.UseEndpoints(endpoints =>            {                endpoints.MapControllers();            });        }

Не забудьте создать БД перед запуском приложения. Для этого в Консоле диспетчера пакетов нужно прописать следующие команды:

Add-Migration init (или любое другое имя)

Update-Database

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

Тестирование

Здесь я покажу как создать UNIT-тесты для контроллера и сервиса. Для тестов я сделал отдельный проект (библиотека классов .Net Core).

Тест для контроллера

public class MainControllerTests    {        [Fact]        public void GetDataMessage()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            var document = GetDoc();            mockDocs.Setup(x => x.GetAll()).Returns(new List<Document> { document });            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Get() as JsonResult;            // Assert            Assert.Equal(new List<Document> { document }, result?.Value);        }        [Fact]        public void GetNotNull()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            mockDocs.Setup(x => x.Create(GetDoc())).Returns(GetDoc());            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Get() as JsonResult;            // Assert            Assert.NotNull(result);        }        [Fact]        public void PostDataMessage()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            mockDocs.Setup(x => x.Create(GetDoc())).Returns(GetDoc());            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Post() as JsonResult;            // Assert            Assert.Equal("Work was successfully done", result?.Value);        }        [Fact]        public void UpdateDataMessage()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            var document = GetDoc();            mockDocs.Setup(x => x.Get(document.Id)).Returns(document);            mockDocs.Setup(x => x.Update(document)).Returns(document);            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Put(document) as JsonResult;            // Assert            Assert.Equal($"Update successful {document.Id}", result?.Value);        }        [Fact]        public void DeleteDataMessage()        {            var mockDocs = new Mock<IBaseRepository<Document>>();            var mockService = new Mock<IRepairService>();            var doc = GetDoc();            mockDocs.Setup(x => x.Get(doc.Id)).Returns(doc);            mockDocs.Setup(x => x.Delete(doc.Id));            // Arrange            MainController controller = new MainController(mockService.Object, mockDocs.Object);            // Act            JsonResult result = controller.Delete(doc.Id) as JsonResult;            // Assert            Assert.Equal("Delete successful", result?.Value);        }        public Document GetDoc()        {            var mockCars = new Mock<IBaseRepository<Car>>();            var mockWorkers = new Mock<IBaseRepository<Worker>>();            var carId = Guid.NewGuid();            var workerId = Guid.NewGuid();            mockCars.Setup(x => x.Create(new Car()            {                Id = carId,                Name = "car",                Number = "123"            }));            mockWorkers.Setup(x => x.Create(new Worker()            {                Id = workerId,                Name = "worker",                Position = "manager",                Telephone = "89165555555"            }));            return new Document            {                Id = Guid.NewGuid(),                CarId = carId,                WorkerId = workerId            };        }    }

В данных тестах проверяется работа каждого метода контроллера на их корректное выполнение.

Тест для сервиса

public class RepairServiceTests    {        [Fact]        public void WorkSuccessTest()        {            var serviceMock = new Mock<IRepairService>();            var mockCars = new Mock<IBaseRepository<Car>>();            var mockWorkers = new Mock<IBaseRepository<Worker>>();            var mockDocs = new Mock<IBaseRepository<Document>>();            var car = CreateCar(Guid.NewGuid());            var worker = CreateWorker(Guid.NewGuid());            var doc = CreateDoc(Guid.NewGuid(), worker.Id, car.Id);            mockCars.Setup(x => x.Create(car)).Returns(car);            mockDocs.Setup(x => x.Create(doc)).Returns(doc);            mockWorkers.Setup(x => x.Create(worker)).Returns(worker);            serviceMock.Object.Work();            serviceMock.Verify(x => x.Work());        }        private Car CreateCar(Guid carId)        {            return new Car()            {                Id = carId,                Name = "car",                Number = "123"            };        }        private Worker CreateWorker(Guid workerId)        {            return new Worker()            {                Id = workerId,                Name = "worker",                Position = "manager",                Telephone = "89165555555"            };        }        private Document CreateDoc(Guid docId, Guid workerId, Guid carId)        {            return new Document            {                Id = docId,                CarId = carId,                WorkerId = workerId            };        }    }

В тесте для сервиса есть всего один тест для метода Work. Тут проверяется отработал этот метод или нет.

Запуск тестов

Чтобы запустить тесты достаточно зайти во вкладку Тест и нажать выполнить все тесты.

Выкладываем в Docker

В финале я покажу, как выложить данное приложение в Docker Hub. В Visual Studio 2019 это сделать крайне просто. Учтите, что у вас уже должен быть профиль в Docker и создан репозиторий в Docker Hub.

Нажимаете ПКМ на ваш проект и выбираете пункт опубликовать.

Там выбираем Docker Container Registry

На следующем окне, надо выбрать Docker Hub

Далее введите свои учетные данный Docker.

Если все прошло успешно, то осталось сделать последнюю вещь, нажать кнопку Опубликовать.

Готово, вы опубликовали свое приложение в Docker Hub!

Заключение

В данной статье я показал, как использовать возможности C# ASP.NET Core для создания простого Rest API. Показал, как создавать модели, записывать их в БД, как создать свой репозиторий, как использовать сервисы и как создавать контроллеры, которые будут отправлять JSON ответы на ваш Front. Также показал, как сделать Unit-тесты для слоев контроллеров и сервисов. И в финале показал, как выложить приложение в Docker.

Надеюсь, что данная статья будет вам полезна!

Подробнее..
Категории: C , Net , Asp.net core , Asp , Rest api

Перевод REST API в Symfony (без FosRestBundle) с использованием JWT аутентификации. Часть 1

06.07.2020 18:16:50 | Автор: admin

Перевод статьи подготовлен в преддверии старта курса Symfony Framework.





В первой части статьи мы рассмотрим самый простой способ реализации REST API в проекте Symfony без использования FosRestBundle. Во второй части, которую я опубликую следом, мы рассмотрим JWT аутентификацию. Прежде чем мы начнем, сперва мы должны понять, что на самом деле означает REST.


Что означает Rest?


REST (Representational State Transfer передача состояния представления) это архитектурный стиль разработки веб-сервисов, который невозможно игнорировать, потому что в современной экосистеме существует большая потребность в создании Restful-приложений. Это может быть связано с уверенным подъемом позиций JavaScript и связанных фреймворков.


REST API использует протокол HTTP. Это означает, что когда клиент делает какой-либо запрос к такому веб-сервису, он может использовать любой из стандартных HTTP-глаголов: GET, POST, DELETE и PUT. Ниже описано, что произойдет, если клиент укажет соответствующий глагол.


  • GET: будет использоваться для получения списка ресурсов или сведений о них.
  • POST: будет использоваться для создания нового ресурса.
  • PUT: будет использоваться для обновления существующего ресурса.
  • DELETE: будет использоваться для удаления существующего ресурса.

REST не имеет состояний (state), и это означает, что на стороне сервера тоже нет никаких состояний запроса. Состояния остаются на стороне клиента (пример использование JWT для аутентификации, с помощью которого мы собираемся засекьюрить наш REST API). Таким образом, при использовании аутентификации в REST API нам нужно отправить аутентификационный заголовок, чтобы получить правильный ответ без хранения состояния.


Создание проекта Symfony:


Во-первых, мы предполагаем, что вы уже установили PHP и менеджер пакетов Сomposer для создания нового проекта Symfony. С этим всем в наличии создайте новый проект с помощью следующей команды в терминале:


composer create-project symfony/skeleton demo_rest_api


Создание проекта Symfony


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



Структура проекта


Config: содержит все настройки бандла и список бандлов в bundle.php.
Public: предоставляет доступ к приложению через index.php.
Src: содержит все контроллеры, модели и сервисы
Var: содержит системные логи и файлы кэша.
Vendor: содержит все внешние пакеты.


Теперь давайте установим некоторые необходимые пакеты с помощью Сomposer:


composer require symfony/orm-packcomposer require sensio/framework-extra-bundle

Мы установили sensio/framework-extra-bundle, который поможет нам упростить код, используя аннотации для определения наших маршрутов.


Нам также необходимо установить symphony/orm-pack для интеграции с Doctrine ORM, чтобы соединиться с базой данных. Ниже приведена конфигурация созданной мной базы данных, которая может быть задана в файле .env.



.env файл конфигурации


Теперь давайте создадим нашу первую сущность. Создайте новый файл с именем Post.php в папке src/Entity.


<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /**  * @ORM\Entity  * @ORM\Table(name="post")  * @ORM\HasLifecycleCallbacks()  */ class Post implements \JsonSerializable {  /**   * @ORM\Column(type="integer")   * @ORM\Id   * @ORM\GeneratedValue(strategy="AUTO")   */  private $id;  /**   * @ORM\Column(type="string", length=100   *   */  private $name;  /**   * @ORM\Column(type="text")   */  private $description;  /**   * @ORM\Column(type="datetime")   */  private $create_date;  /**   * @return mixed   */  public function getId()  {   return $this->id;  }  /**   * @param mixed $id   */  public function setId($id)  {   $this->id = $id;  }  /**   * @return mixed   */  public function getName()  {   return $this->name;  }  /**   * @param mixed $name   */  public function setName($name)  {   $this->name = $name;  }  /**   * @return mixed   */  public function getDescription()  {   return $this->description;  }  /**   * @param mixed $description   */  public function setDescription($description)  {   $this->description = $description;  }  /**   * @return mixed   */  public function getCreateDate(): ?\DateTime  {   return $this->create_date;  }  /**   * @param \DateTime $create_date   * @return Post   */  public function setCreateDate(\DateTime $create_date): self  {   $this->create_date = $create_date;   return $this;  }  /**   * @throws \Exception   * @ORM\PrePersist()   */  public function beforeSave(){   $this->create_date = new \DateTime('now', new \DateTimeZone('Africa/Casablanca'));  }  /**   * Specify data which should be serialized to JSON   * @link https://php.net/manual/en/jsonserializable.jsonserialize.php   * @return mixed data which can be serialized by <b>json_encode</b>,   * which is a value of any type other than a resource.   * @since 5.4.0   */  public function jsonSerialize()  {   return [    "name" => $this->getName(),    "description" => $this->getDescription()   ];  } }

И после этого выполните команду: php bin/console doctrine:schema:create для создания таблицы базы данных в соответствии с нашей сущностью Post.


Теперь давайте создадим PostController.php, куда мы добавим все методы, взаимодействующие с API. Он должен быть помещен в папку src/Controller.


<?php /**  * Created by PhpStorm.  * User: hicham benkachoud  * Date: 02/01/2020  * Time: 22:44  */ namespace App\Controller; use App\Entity\Post; use App\Repository\PostRepository; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; /**  * Class PostController  * @package App\Controller  * @Route("/api", name="post_api")  */ class PostController extends AbstractController {  /**   * @param PostRepository $postRepository   * @return JsonResponse   * @Route("/posts", name="posts", methods={"GET"})   */  public function getPosts(PostRepository $postRepository){   $data = $postRepository->findAll();   return $this->response($data);  }  /**   * @param Request $request   * @param EntityManagerInterface $entityManager   * @param PostRepository $postRepository   * @return JsonResponse   * @throws \Exception   * @Route("/posts", name="posts_add", methods={"POST"})   */  public function addPost(Request $request, EntityManagerInterface $entityManager, PostRepository $postRepository){   try{    $request = $this->transformJsonBody($request);    if (!$request || !$request->get('name') || !$request->request->get('description')){     throw new \Exception();    }    $post = new Post();    $post->setName($request->get('name'));    $post->setDescription($request->get('description'));    $entityManager->persist($post);    $entityManager->flush();    $data = [     'status' => 200,     'success' => "Post added successfully",    ];    return $this->response($data);   }catch (\Exception $e){    $data = [     'status' => 422,     'errors' => "Data no valid",    ];    return $this->response($data, 422);   }  }  /**   * @param PostRepository $postRepository   * @param $id   * @return JsonResponse   * @Route("/posts/{id}", name="posts_get", methods={"GET"})   */  public function getPost(PostRepository $postRepository, $id){   $post = $postRepository->find($id);   if (!$post){    $data = [     'status' => 404,     'errors' => "Post not found",    ];    return $this->response($data, 404);   }   return $this->response($post);  }  /**   * @param Request $request   * @param EntityManagerInterface $entityManager   * @param PostRepository $postRepository   * @param $id   * @return JsonResponse   * @Route("/posts/{id}", name="posts_put", methods={"PUT"})   */  public function updatePost(Request $request, EntityManagerInterface $entityManager, PostRepository $postRepository, $id){   try{    $post = $postRepository->find($id);    if (!$post){     $data = [      'status' => 404,      'errors' => "Post not found",     ];     return $this->response($data, 404);    }    $request = $this->transformJsonBody($request);    if (!$request || !$request->get('name') || !$request->request->get('description')){     throw new \Exception();    }    $post->setName($request->get('name'));    $post->setDescription($request->get('description'));    $entityManager->flush();    $data = [     'status' => 200,     'errors' => "Post updated successfully",    ];    return $this->response($data);   }catch (\Exception $e){    $data = [     'status' => 422,     'errors' => "Data no valid",    ];    return $this->response($data, 422);   }  }  /**   * @param PostRepository $postRepository   * @param $id   * @return JsonResponse   * @Route("/posts/{id}", name="posts_delete", methods={"DELETE"})   */  public function deletePost(EntityManagerInterface $entityManager, PostRepository $postRepository, $id){   $post = $postRepository->find($id);   if (!$post){    $data = [     'status' => 404,     'errors' => "Post not found",    ];    return $this->response($data, 404);   }   $entityManager->remove($post);   $entityManager->flush();   $data = [    'status' => 200,    'errors' => "Post deleted successfully",   ];   return $this->response($data);  }  /**   * Returns a JSON response   *   * @param array $data   * @param $status   * @param array $headers   * @return JsonResponse   */  public function response($data, $status = 200, $headers = [])  {   return new JsonResponse($data, $status, $headers);  }  protected function transformJsonBody(\Symfony\Component\HttpFoundation\Request $request)  {   $data = json_decode($request->getContent(), true);   if ($data === null) {    return $request;   }   $request->request->replace($data);   return $request;  } }

Здесь мы определили пять маршрутов:


GET /api/posts: вернет список постов.



api получения всех постов


POST /api/posts: создаст новый пост.



api добавления нового поста


GET /api/posts/id: вернет пост, соответствующий определенному идентификатору,



получение конкретного поста


PUT /api/posts/id: обновит пост.



обновление поста


Это результат после обновления:



пост после обновления


DELETE /api/posts/id: удалит пост.



удаление поста


Это результат получения всех постов после удаления поста с идентификатором 3:



все посты после удаления


Исходный код можно найти здесь


Заключение


Итак, теперь мы понимаем, что такое REST и Restful. Restful API должен быть без состояний. Мы знаем, как создать Restful-приложение, используя HTTP-глаголы. В общем, теперь мы хорошо понимаем REST и готовы профессионально создавать Restful-приложения.


В следующей статье мы рассмотрим, как обеспечить секьюрность API с помощью JWT аутентификации.




Узнать подробнее о курсе Symfony Framework



Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru