Привет, Хабр! Меня зовут Ринат. Я руководитель отдела 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
Что должны хранить схемы (минимум):
Доступные URL для запроса
Доступные GET и POST параметры
Схема ответа
В идеале бы привести схемы к формату 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 — фильтр результатов
Важно понимать — это вкусовщина, все зависит от наличия ресурсов. Главным в таких задачах является построение правильной архитектуры, чтобы не выстрелить себе в ногу (или в колено).
Что еще осталось сделать:
Создаем реестр ключей для партнерских сервисов любым удобным для вас способом, который будет хранить сам ключ и тип сервиса, например:
{
"{key1}": {
"api_type": "service_a"
},
"{key2}": {
"api_type": "service_b"
}
}
Также здесь может храниться любая дополнительная информация (общий лимит по запросам, разрешенные хосты и др.).
Отдаем ключи сервисам и ждем запросов. В любой момент удаляем ключ из базы, чтобы ограничить доступ.
Презентация и вымышленные кейсы
Все готово для тестирования, так давайте же посмотрим что у нас получилось!
Предположим, что наша система занимается бронированием времени в абстрактной организации. Частные 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. С помощью него мы существенно сократили срок разработки и открыли дополнительные возможности для внешних интеграций без больших потерь времени.
И вот, собственно, о времени. Мы все перфекционисты, чем быстрее к нам придет это понимание — тем лучше. Но наш код никогда не будет идеален. Особенно в условиях работы с бизнес‑задачами, ведь для них разработчика ставят в жесткие временные рамки. Результата ждут руководство, коллеги из других отделов и, конечно же, клиент. Ограниченный временной ресурс нужно тратить с умом, отдавая приоритет архитектуре, а не стилю. Потому что на этапе рефакторинга краеугольным камнем станет именно архитектура, ее четкость и простота. Продуманный стиль и красивый визуал едва ли помогут при внесении дополнительного функционала. Тем более в ограниченный промежуток времени.