Где-то на просторах мультивселенной…
Представьте на минуту, что вы капитан Сиракузии, которая в 239 году до н. э. приближается к острову Фарос, что близ города Александрии. Вследствие узости прохода, войти в гавань Александрии — непростая задача, в особенности, для такого корабля, как ваш. Вы слышали, что за последний год навигация около острова улучшилась, так как был завершен Фа́росский маяк, уникальное сооружение, заложенное еще при Птолемее I.
Но, приблизившись к острову, вы понимаете, что промахнулись мимо гавани, так как гигантский маяк был спроектирован башней под воду, а свет сигнальных костров виден только в сумерках и при сильной облачности, ведь зеркала подводной башни отражают свет исключительно вверх. Проклиная свою работу, вы пишете письмо Гиерону II, с описанием того, как неудачно спроектирован интерфейс взаимодействия с портом, а он, как человек с поистине царским чутьем, решает подарить корабль правителю Александрии, а сам начинает смотреть в сторону альтернативных торговых маршрутов…
Почему-то, когда я думаю об интеграционных API, с которыми неудобно работать, в голове появляется образ именно такого вот маяка, и в этой статье хотелось бы рассказать о конкретных проблемах, с которыми мы в Smartcat сталкивались в процессе интеграции с различными внешними сервисами, а также высказать своё субъективное мнение о том, что именно делает интеграционное API удобным. Итак, капитан очевидность поднялся на борт, поплыли.
Проблема #1: external_id курильщика
На мой взгляд, external_id – незаменимая концепция при проектировании API. Идентификатор, сгенеренный на стороне потребителя API и задаваемый при создании сущности, обеспечивает её уникальность и позволяет однозначно идентифицировать сущность во внешней системе. Допустим, мы хотим создать транзакцию на стороне платежного провайдера, если положить в поле external_id наш идентификатор заказа, то при повторной попытке создать транзакцию с таким идентификатором мы получим ошибку и защитимся от создания дубликата.
Не все интеграционные API поддерживают этот концепт, из-за чего приходится писать дополнительный код, обрабатывающий различные сценарии отказов в работе с API. Всегда перед созданием сущности приходится искать её по каким-то дополнительным признакам во внешней системе, и, только после этого что-то создавать. Сам процесс создания в таком случае необходимо явно ограничивать до одного потока выполнения, чтобы избежать проблем с конкурентностью.
Как эту концепцию можно превратить в «подводный маяк»? Ну, например, один из интегрируемых сервисов позволял задавать external_id, который обеспечивал уникальность в течении приблизительного промежуток времени с момента создания сущности, после чего его можно использовать повторно. В отсутствии четкого способа идентифицировать данные при поиске - это проблема. Еще у одного сервиса поиск по external_id работал только два месяца, после чего данные пропадали из выдачи.
Проблема #2: нет разделения моделей для запросов и ответов
Иногда в документации к API описываются модели, которые используются и в запросах к серверу, и в ответах – я называю такие модели Jack of all trades (мастер на все руки). Ничего страшного в этом вроде нет, ровно до тех пор, пока у нас не появляются объекты вроде{ foo:”value1”, bar:”value2” … }
, где поле “foo” заполняется в моделях запроса, а “bar” - в ответах сервера. Но это уже выясняется эмпирически в процессе интеграции.
На мой взгляд, контекст моделей участвующие в запросах и ответах лучше сразу разделять: да, мы получаем оверхед по поддержке большего числа моделей (современные IDE с инструментами по рефакторингу и навигации позволяют ощутимо его снизить), но зато мы получаем возможность работать с ними независимо друг от друга и снимаем когнитивную нагрузку с потребителей нашего API.
Проблема #3: избыточная защита в неожиданных местах
Вполне обоснованно ожидать, что сервис, выполняющий асинхронные операции над данными, будет отправлять callback-уведомления об изменении созданных потребителем API сущностей — это позволяет снизить частоту опроса состояния сущностей на стороне потребителя, сохранив возможность получать информацию по изменениям максимально оперативно.
Хорошая практика - убедиться, что получатель колбека действительно его получил - обычно достаточно проверить код ответа: 200 — порядок, у нас в клубе джентльменам принято верить на слово. Если хочется подстраховаться, то можно договориться, что получатель, например, будет передавать в ответе идентификатор колбека. Так по крайней мере мы будем уверены, что объект получен, и мы не стучимся в абстрактный эндпоинт в вакууме, возвращающий на любой запрос 200.
Но однажды мы встретили ситуацию, которую назвали «MITM-рояль в кустах»: для того, чтобы просто сообщить отправителю о том, что колбек получен, необходимо прочитать всю модель, и вернуть в ответе хеш на основании её полей. Само собой, в том порядке, в котором они идут в JSON — отложим в сторону «новомодные» фреймворки с маппингами в типизированные модели.
То есть несмотря на то, что по идее мы защищены https-сертификатом, разработчики «городят» дополнительный слой безопасности в идентификации получения коллбека.
Еще одно гипотетическое неудобство такого подхода — сервис отправитель принуждает нас разбирать и обрабатывать (пусть даже частично) модель коллбека синхронно, при получении. Хотя при работе с несколькими типами событий через один эндпоинт вариант – сохранить «сырой» коллбек, а обработку провести асинхронно - может быть предпочтительнее.
А вы часто встречаете реализацию защиты от MITM-атаки при обработке коллбека?
Проблема #4: изменение типа в зависимости от параметров
{transaction: {id:”1”}}
{transaction: [{id:”1”}]}
Как вам метод, который в зависимости от параметра в запросе будет возвращать в одном из полей ответа либо объект, либо массив, содержащий в себе один элемент?
Я не вижу ничего плохого в том, чтобы давать потребителю возможность кастомизировать получаемый им ответ (в разумных пределах, само собой), но одно дело – добавить дополнительные необязательные поля в модель, которые будут заполняться в зависимости от параметров, и, совершенно другое – менять тип уже существующего поля, при этом, не обозначить это явно в документации.
Особенно неприятно то, что, при работе с JSON, например, через библиотеку Newtonsoft Json.NET вы будете получать высокоуровневую ошибку десериализации поля, а дальше долго и вдумчиво всматриваться в два одинаковых объекта, пока глаз не заметит пресловутую пару квадратных скобок…
Проблема #5: делегированная безопасность
Представьте, что у вас есть интернет магазин, и вы хотите принимать оплату покупок в нем онлайн по карте, давая возможность пользователю запоминать данные его карты, но совсем не хотите заморачиваться с полноценным прохождением сертификации по PCI DSS.
На помощь придет огромное множество платежных систем, которые берут на себя всю работу по привязке карты пользователя. По завершению вы получите идентификатор карты во внешней системе, и наверняка – маскированный номер карты. Эти данные вы спокойно сохраняете у себя в БД магазина для дальнейшего использования.
Но есть нюанс. Если платежная система в качестве такого идентификатора возвращает её хешированный номер, то в случае, если наша база будет скомпрометирована, наличие хеша, бина и последних четырех цифр карты позволят злоумышленникам за обозримое время перебором получить полный номер карты. И тут уже никакая блокировка идентификаторов не поможет.
Заключение: как всё исправить
Конечно, перечисленные в статье примеры – самые «выдающиеся» представители подводных маяков. Если повезет, то вы никогда с ними не познакомитесь. Но объединяет все эти примеры, на мой взгляд, одно – в них явно не были продуманы сценарии использования с точки зрения потребителей (о злом умысле при проектировании вопрос ставить желания нет).
Поэтому возникает вопрос, а всегда ли мы при создании API думаем о его конечных пользователях? Чтобы сделать интерфейс по-настоящему удобным, стоит учитывать ряд моментов:
-
Внутренняя кухня приложения не должна влиять на интеграцию. «Обеспечение уникальности в течение интервала ~N» – явно как-то связано с ненужными внешнему пользователю особенностями системы. Функции external_id должны быть одинаковыми на протяжение всего времени жизни сущности в системе.
-
Контекст моделей запросов и ответов нужно разделять. Может показаться, что это сильно избыточно. Однако в конечном итоге, максимальная прозрачность в вопросе использования моделей даёт плюсы не только потребителям API, но и разработчикам.
-
Выбирайте подходящие методы защиты. Реализуя защиту от какой-либо атаки задумайтесь, действительно ли этот вектор атаки актуален в реализуемом вами сценарии.
-
Нужно избегать чрезмерной кастомизации ответов. Модель ответа – более-менее строгий контракт, излишняя кастомизация которого может говорить о том, что рядом нужен просто еще один, другой контракт.
-
Думайте о том, как передаваемые данные могут быть использованы злоумышленниками. Всегда нужны серьезные основания для того, чтобы отдавать чувствительные данные во внешние системы, даже в зашифрованном виде, особенно если эти данные используются для идентификации. Компрометации данных на стороне потребителя может обесценить любые удобства таких идентификаторов.
А какие трудности при работе над интеграционными проектами встречались вам? Делитесь описанием и своими решениями/советами в комментариях – сделаем материал полезнее.
Автор: ComradeAlpha