Разделяй и властвуй: как мы адаптировали API монолитного приложения для нужд публичных запросов и внешних интеграций

—

от автора

Привет, Хабр! Меня зовут Ринат. Я руководитель отдела backend-разработки компании AppEvent. 

Представьте: к вам в компанию обратились «Сервис А» и «Сервис В». При сотрудничестве обоих сервисов с вашей компанией нужно открыть часть функционала «Сервис А» и часть функционала «Сервис В». У «Сервис А» не должно быть доступа к функционалу для «Сервис В».

Эту задачу нужно реализовать в условиях сложной бизнес-логики и с монолитным приложением на {не самый популярный ЯП}. 

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

Размышляем о возможных решениях

После проработки нескольких решений потопал к руководству для согласования. В кабинете происходит такой вот диалог:

Я: Нужно идти в ногу со временем! Никто не пишет на {не самый популярный ЯП}, все давно пишут на {более популярный ЯП}. Это хороший шанс для перевода части функционала на современные стандарты

Руководство: Сколько?

Я: Много

Руководство: Нужно быстрее. Давай сделаем чтобы работало, а потом перепишем! 

Когда IT работает на бизнес (клиента, уровень продаж, успех продукта), скорость становится определяющим фактором. И как бы ни хотелось обмазаться новыми технологиями, приходится работать с текущей кодовой базой для большей скорости. 

Выбранное решение

Решение нужно было дорабатывать с учетом новых вводных. И, о чудо, на глаза попались наши частные API для мобильного приложения. Оказалось, что они покрывают 95% нужд партнерских сервисов. Но у API был один недостаток — их интерфейс был слишком обширен. Было принято решение написать middleware контроля доступа для наших частных API.

Описание решения

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

синий - существующая логика, зеленый - новая логика

синий — существующая логика, зеленый — новая логика

Понимаю, не rocket science, но свою задачу middleware выполняет. Давайте пройдемся по модулям, которые заслуживают отдельного внимания.

Представьте: middleware — это сторожевой пес, запросы — это входящие и выходящие люди, территория дома — приложение. Как и каждый «хороший мальчик» наш пес может делать несколько вещей:

schemes
Как и любую собаку, нашего подопечного необходимо обучить нескольким вещам:

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

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

requestCatcher

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

security

Пес начинает принюхиваться, его задача: 

  • убедиться, что человек уже был у вас раньше (проверка подлинности подписи запроса, проверка типа API);

  • в дом можно, в сарай нет — там хозяин хранит самые интересные вещи (присутствует ли запрашиваемый url в схеме для данного вида API);

  • не слишком часто ли данный человек приходит (защита от DDoS-атак);

  • если собака обучена для поиска запрещенных предметов (другие типы защиты публичных API), то сейчас самое время. 

Если все проверки прошли успешно, пес довольно кивнет головой, и пропустит во двор. В ином случае — прогонит человека прочь. 

inputFilter 

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

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

outputFilter 

Идет время. Наш пес спокойно догрызает остатки отобранной колбасы, как вдруг открывается дверь. Реакция зависит от того, кто пришел:

  • Если это хозяин, не обращаем внимания. Даже если он пытается вынести все из дома. Мало ли, вдруг переезжает;

  • Если это отмеченный человек, то приходится проверять, что можно выносить этому человеку из дома. Изымать все, что выносить не положено (сверяемся со схемой и отдаем только разрешенные данные).

errorHandler 

А что если человек не выйдет из дома, а вылетит из окна? (бизнес-логика вернет ошибку). Тогда наш пес подберет подходящее действие для каждого типа гостя: на кого-то просто полает, кого-то укусит за ногу, а кого-то проигнорирует (стилизуем ошибки бизнес-логики под требования партнерских систем).

Как думаете, наш пес — «хороший мальчик»?

Чуть подробнее поговорим о схемах. Каждый сервис имеет свою схему с соответствующим названием, в нашем случае это — service_a.json и service_b.json

Что должны хранить схемы (минимум): 

  1. Доступные URL для запроса

  2. Доступные GET и POST параметры

  3. Схема ответа

В идеале бы привести схемы к формату openAPI, чтобы убить сразу 2 зайцев (фильтровать запросы и выводить документацию для партнеров).

Пример схемы
{   "/orders/": {     "allowed_methods": [       "GET",       "POST"     ],     "content_type": [       "application/json"     ],     "request_limit": {       "count": 5,       "seconds": 1     },     "query_params": {       "status": {         "type": "str",         "enum": [           "request",           "booked"         ],         "required": true       }     },     "json": {       "date_start": {         "type": "int",         "required": true       },       "duration": {         "type": "int",         "required": true       },       "client": {         "type": "object",         "required": true,         "schema": {           "fio": {             "type": "str",             "required": true           },           "email": {             "type": "email",             "required": true           }         }       },       "status": {         "type": "str",         "enum": [           "request"         ],         "required": true       }     },     "response": {       "id": true,       "date_start": true,       "duration": true,       "client": {         "fio": true,         "email": true       },       "status": true     }   },   "/orders/{int}/": {     "allowed_methods": [       "GET",       "POST"     ],     "content_type": [       "application/json"     ],     "json": {       "status": {         "type": "str",         "enum": [           "request",           "booked"         ],         "required": true       }     },     "response": {       "id": true,       "date_start": true,       "duration": true,       "client": {         "fio": true,         "email": true       },       "status": true     }   } }

Пройдемся по пунктам:

Ключ — уникальный шаблон url (может иметь любую форму, зависимо от используемых инструментов)

allowed_methods — разрешенные методы (актуально, если у вас на один url завязано несколько действий)

content_type — ограничения по типу контента (в основном для безопасности)

request_limit — ограничения по количеству запросов (актуально, если на отдельные url свой лимит)

query_params — ограничения по параметрам запроса

json — ограничения по принимаемым параметрам

response — фильтр результатов

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

Что еще осталось сделать:

  1. Создаем реестр ключей для партнерских сервисов любым удобным для вас способом, который будет хранить сам ключ и тип сервиса, например:

{   "{key1}": {     "api_type": "service_a"   },   "{key2}": {     "api_type": "service_b"   } }

Также здесь может храниться любая дополнительная информация (общий лимит по запросам, разрешенные хосты и др.).

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

Презентация и вымышленные кейсы

Все готово для тестирования, так давайте же посмотрим что у нас получилось!

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

Получение списка бронирований

GET myapp.ru/orders/

GET PARAMS: status

RESPONSE:

[ { "id": int, "date_start": timestamp, "duration": int, "client": { "id": int, "fio": string, "email": string }, "status": enum["request", "booked", "declined"] } ]

Создание бронирования

POST myapp.ru/orders/

REQUEST:

{     "date_start": timestamp,     "duration": int,     "client": {         "fio": string,         "email": string     },     "status": enum["request", "booked", "declined"] }

RESPONSE:

{     "id": int,     "date_start": timestamp,     "duration": int,     "client": {         "id": int,         "fio": string,         "email": string     },     "status": enum["request", "booked", "declined"] }

Изменение бронирования

POST myapp.ru/orders/{id:int}/

REQUEST:

{     "date_start": timestamp,     "duration": int,     "client": {         "fio": string,         "email": string     },     "status": enum["request", "booked", "declined"] }

RESPONSE:

{     "id": int,     "date_start": timestamp,     "duration": int,     "client": {         "id": int,         "fio": string,         "email": string     },     "status": enum["request", "booked", "declined"] }

Теперь требования наших партнеров:

Сервис А: видеть все наши заявки и брони, создавать заявки

Сервис В: видеть все заявки и брони, создавать заявки, видеть клиентов, переводить заявки в брони и наоборот

Схема “Сервис А”
{   "/orders/": {     "allowed_methods": [       "GET",       "POST"     ],     "content_type": [       "application/json"     ],     "query_params": {       "status": {         "type": "str",         "enum": [           "request",           "booked"         ],         "required": true       }     },     "json": {       "date_start": {         "type": "int",         "required": true       },       "duration": {         "type": "int",         "required": true       },       "client": {         "type": "object",         "required": true,         "schema": {           "fio": {             "type": "str",             "required": true           },           "email": {             "type": "email",             "required": true           }         }       },       "status": {         "type": "str",         "enum": [           "request"         ],         "required": true       }     },     "response": {       "id": true,       "date_start": true,       "duration": true,       "status": true     }   } }

Схема “Сервис В”
{   "/orders/": {     "allowed_methods": [       "GET",       "POST"     ],     "content_type": [       "application/json"     ],     "query_params": {       "status": {         "type": "str",         "enum": [           "request",           "booked"         ],         "required": true       }     },     "json": {       "date_start": {         "type": "int",         "required": true       },       "duration": {         "type": "int",         "required": true       },       "client": {         "type": "object",         "required": true,         "schema": {           "fio": {             "type": "str",             "required": true           },           "email": {             "type": "email",             "required": true           }         }       },       "status": {         "type": "str",         "enum": [           "request"         ],         "required": true       }     },     "response": {       "id": true,       "date_start": true,       "duration": true,       "client": {         "fio": true,         "email": true       },       "status": true     }   },   "/orders/{int}/": {     "allowed_methods": [       "GET",       "POST"     ],     "content_type": [       "application/json"     ],     "json": {       "status": {         "type": "str",         "enum": [           "request",           "booked"         ],         "required": true       }     },     "response": {       "id": true,       "date_start": true,       "duration": true,       "client": {         "fio": true,         "email": true       },       "status": true     }   } }

Заключение

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

И вот, собственно, о времени. Мы все перфекционисты, чем быстрее к нам придет это понимание — тем лучше. Но наш код никогда не будет идеален. Особенно в условиях работы с бизнес‑задачами, ведь для них разработчика ставят в жесткие временные рамки. Результата ждут руководство, коллеги из других отделов и, конечно же, клиент. Ограниченный временной ресурс нужно тратить с умом, отдавая приоритет архитектуре, а не стилю. Потому что на этапе рефакторинга краеугольным камнем станет именно архитектура, ее четкость и простота. Продуманный стиль и красивый визуал едва ли помогут при внесении дополнительного функционала. Тем более в ограниченный промежуток времени.


ссылка на оригинал статьи https://habr.com/ru/articles/783562/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *