Привет, Хабр! Меня зовут Ринат. Я руководитель отдела 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. С помощью него мы существенно сократили срок разработки и открыли дополнительные возможности для внешних интеграций без больших потерь времени.

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

Комментарии (0)