Статьи

Версии вашего API неверны

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

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

Вообразите это:

HTTP GET:
https://haveibeenpwned.com/api/breachedaccount/foo

Response:
["Adobe","Gawker"]

Это было просто отлично. Когда я строил Был ли я забит? (HIBP) в конце ноября это был простой и быстрый сервис, которым могли пользоваться несколько человек. Я думаю, что будет справедливо сказать, что первые два очка были достигнуты, но не последний. Это было не «несколько», фактически к концу первой недели это было больше, чем Google Analytics мог обработать . Дело в том, что вы не всегда можете предсказать будущее, когда пишете свой API, и в какой-то момент вам, возможно, придется изменить что-то, от чего люди уже зависят.

Но вот проблема — каждый раз, когда вы начинаете говорить о том, что связано с API через HTTP, это происходит:

НО ЭТО НЕ "ОТДЫХАЕТ" ЕСЛИ ВЫ ... ХОРОШО!

Каждый раз, когда вы поворачиваете, есть разные философские взгляды на «правильный путь» и множество взад и вперед на REST, что такое RESTful, что нет, и если это вообще имеет значение. Давайте поговорим об изменениях API, влиянии на управление версиями, почему существует так много разных идей о том, как это должно быть сделано, и в конечном итоге, почему ни один из них не так важен, как фактическая работа.

Вытащить больше данных о нарушении

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

["BattlefieldHeroes","Gawker"]

Зачем? Потому что «BattlefieldHeroes» написан на Pascal, что отлично подходит для сопоставления его с жестко закодированными классами CSS (хотя, вероятно, это не очень хороший долгосрочный подход) и для того, чтобы иметь «стабильное» имя для ссылки (я не буду его менять , даже если есть второе нарушение), но это не подходит для отображения в качестве заголовка. Все это происходит из хранилища таблиц Azure, а затем я перехожу в SQL Azure для извлечения реляционных данных, которые фактически описывают нарушение. Одним из атрибутов в этом реляционном хранилище является имя, которое вы видите выше.

То, что я действительно хотел сделать, было что-то вроде этого:

[
  {
    "Name":"BattlefieldHeroes",
    "Title":"Battlefield Heroes",
    "BreachDate":"2011-06-26",
    "AddedDate":"2014-01-23T13:10Z",
    "PwnCount":530270,
    "Description":"In June 2011 as part of a final breached data dump, the hacker collective "LulzSec" <a href=” http://www.rockpapershotgun.com/2011/06/26/lulzsec-over-release-battlefield-heroes-data/ “>obtained and released over half a million usernames and passwords from the game Battlefield Heroes</a>. The passwords were stored as MD5 hashes with no salt and many were easily converted back to their plain text versions.",
    "DataClasses":["Password","Username"]
  },
  {
    "Name":"Gawker",
    "Title":"Gawker",
    "BreachDate":"2010-12-11",
    "AddedDate":"2013-12-04T00:00Z",
    "PwnCount":1247574,
    "Description":"In December 2010, Gawker was attacked by the hacker collective "Gnosis" in retaliation for what was reported to be a feud between Gawker and 4Chan. Information about Gawkers 1.3M users was published along with the data from Gawker's other web presences including Gizmodo and Lifehacker. Due to the prevalence of password reuse, many victims of the breach <a href=\"http://www.troyhunt.com/2011/01/why-your-apps-security-design-could.html\">then had their Twitter accounts compromised to send Acai berry spam</a>.",
    "DataClasses":["Email","Password","Username"]
  }
]

Возьми? К предыдущему пункту имени нарушения, он все еще там в атрибуте имени, но теперь у нас также есть заголовок. Это то, что вы показываете людям — «Герои поля битвы» — но что еще более важно, если Gawker снова ударит, я могу назвать нарушение как Gawker2014, и название может быть чем-то дружественным по типу «Gawker (атака сирийской электронной армии)» , Он сегментирует то, что является стабильным и предсказуемым, из того, что нет, и это означает, что люди могут создавать зависимости, такие как изображения или другие активы, от атрибута имени.

Другие данные должны быть достаточно ясными: дата нарушения, когда он был добавлен в систему, количество учетных записей, которые были открыты, описание нарушения (опять же, это может измениться, если необходимо изменить словесность) и «DataClasses ». Одной из вещей, о которой многие спрашивали, было описание того, что было скомпрометировано в результате взлома, поэтому теперь существует целая куча атрибутов, которые можно добавить через коллекцию в самом взломе. Я уже показываю их под каждым нарушением на странице веб-сайтов Pwned (это еще одна причина, по которой я теперь могу подправить некоторые описания).

Это серьезное изменение. Хотя отношение к API одинаково — укажите имя учетной записи, верните список нарушений — больше нет строкового массива имен нарушений. Если бы я просто заменил старый API на этот, все сломалось бы. API. Должен. Evolve.

Программное обеспечение развивается, API должны быть версионными

Давайте просто проясним это: мир движется дальше. API для HIBP длился около 2 месяцев, не потому, что он был плохо спроектирован, а потому, что сервис стал дико, неожиданно успешным. Мне нравятся такие проблемы, как и вам.

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

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

Различные лагеря версий

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

  1. URL: вы просто вбиваете версию API в URL, например: https://haveibeenpwned.com/api/v2/breachedaccount/foo
  2. Пользовательский заголовок запроса: вы используете тот же URL, что и раньше, но добавляете заголовок, такой как «api-version: 2»
  3. Заголовок Accept: Вы изменяете заголовок accept, чтобы указать версию, например «Accept: application / vnd.haveibeenpwned.v2 + json»

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

  1. URL-адреса отстойны, потому что они должны представлять сущность: на самом деле я с этим согласен, поскольку извлекаемая мной сущность является взломанной, а не версией взломанной учетной записи. Семантически, это не совсем правильно, но, черт возьми, это просто в использовании!
  2. Заголовки пользовательских запросов — отстой, потому что на самом деле это не семантический способ описания ресурса: спецификация HTTP дает нам возможность запрашивать природу, в которой мы хотим, чтобы ресурс был представлен в виде заголовка accept, зачем воспроизводить это?
  3. Заголовки Accept — это отстой, потому что их сложнее тестировать: я больше не могу просто дать кому-то URL-адрес и сказать «Вот, кликни на это», вместо этого им нужно тщательно составить запрос и настроить заголовок accept соответствующим образом.

Различные аргументы за и против каждого подхода имеют тенденцию идти от «Это« правильный »способ сделать это, но менее практичный» до «Это самый простой способ создать что-то расходное, что делает его« правильным »». Существует много дискуссий о гипермедиа, согласовании контента, что такое «ОТДЫХ» и всякие другие вопросы. К сожалению, это очень часто становится философским и упускает из виду, какова должна быть реальная цель: создание программного обеспечения, которое работает, и особенно для API, делая его легко потребляемым.

Речь идет о стабильном контракте, глупый!

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

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

Вот 3 неправильных способа использования HIBP API, из которых вы можете выбрать

Хорошо, теперь, когда мы четко определили, что вы делаете неправильно, я бы хотел дать вам возможность выбрать один из 3 неправильных способов. Чего ждать?! Это так: как бы я ни реализовывал API, его будет слишком сложно потреблять, слишком академично, слишком вероятно, что он потерпит неудачу на прокси-сервере, или что-то еще. Вместо того, чтобы выбрать 1 неправильный путь, я решил дать вам все 3 неправильных пути, и вы можете выбрать тот, который наименее неправильный для вас.

Неправильный способ 1 — управление версиями URL:

HTTP GET:
https://haveibeenpwned.com/api/v2/breachedaccount/foo

Неправильный способ 2 — пользовательский заголовок запроса:

HTTP GET:
https://haveibeenpwned.com/api/breachedaccount/foo
api-version: 2

Неверный путь 3 — тип контента:

HTTP GET:
https://haveibeenpwned.com/api/breachedaccount/foo
Accept: application/vnd.haveibeenpwned.v2+json

Неизбежно, кто-то скажет мне, что предоставление 3 неправильных путей — неправильная вещь. Разве это не означает, что нужно поддерживать больше кода? Нет, это просто означает, что базовая реализация Web API имеет два атрибута:

[VersionedRoute("api/breachedaccount/{account}", 2)]
[Route("api/v2/breachedaccount/{account}")]
public IEnumerable<Breach> GetV2(string account)

Первый — это просто ограничение маршрутизации, которое реализует RouteFactoryAttribute. Я передаю маршрут и передаю версию, которая может отображаться на этот маршрут, после чего реализация ищет наличие заголовка «api-version» или заголовка accept, соответствующего этому шаблону:

@"application\/vnd\.haveibeenpwned\.v([\d]+)\+json"

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

Второй атрибут, описывающий метод GetV2, приведенный выше, предоставлен Web API 2 и функцией маршрутизации атрибутов . Конечно, мы всегда можем выполнять маршрутизацию в Web API, но обычно она определяется глобально. Подобная маршрутизация атрибутов переносит определение маршрута в контекст, в котором он применяется, и упрощает просмотр того, какое действие контроллера будет вызываться каким маршрутом. Это также означает, что реализации всех трех неправильных способов вызова API находятся в одном удобном месте.

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

Но что, если вы не укажете версию?

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

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

У вас есть 3 варианта, но мои личные предпочтения …

У меня есть роскошь контролировать и API, и основным потребителем этого является сам сайт HIBP. Учитывая, что я предоставил 3 варианта использования API, какой я сам использую?

Я пошел с философским фаворитом, который должен указать это через заголовок принятия. Я не думаю, что это правильно, а другие — нет, скорее, я думаю, что это имеет смысл по двум основным причинам:

  1. Я согласен, что URL не должен изменяться: если мы согласны с тем, что URL представляет ресурс, то, если мы не пытаемся представлять разные версии самого ресурса, то нет, я не думаю, что URL должен измениться. Нарушения для foo всегда являются нарушениями для foo, и я не думаю, что только из-за того, что я изменяю данные, возвращаемые для foo, расположение foo должно измениться.
  2. Я согласен, что заголовки accept описывают, как вам нужны данные: это семантика спецификации HTTP, и так же, как семантика глаголов запроса имеет большой смысл (т.е. мы получаем или помещаем, удаляем или публикуем), так же делает так, как клиент хотел бы, чтобы содержимое отображалось.

Это ни в коем случае не означает, что я думаю, что другие два неверны и, честно говоря, нет лучшего способа поделиться API с кем-либо, чем сказать «Здесь, нажмите здесь», но когда я могу легко сконструировать запрос и управлять заголовками, Я пошел с этим маршрутом.

Собственно, если подумать, я тоже использую версию в доменном маршруте. Зачем? Просто в процессе написания этого API я постоянно общался с людьми о способах его запроса (подробнее об этом позже) и об атрибутах, которые он возвращает. Возможность щелкнуть по электронной почте и сказать «Эй, вот что я думаю», и они просто щелкают по ней и получают результаты, что бесценно. Именно это справедливо утверждают сторонники подхода к управлению версиями URL: вы просто не можете сделать это, когда зависите от заголовков.

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

В заключение

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

Если я позволю другим подумать о том, как создавать версии своих API, с последней мыслью: никто не будет использовать ваш API, пока вы его не создадите. Хватит откладывать. Ничто из этого не является «плохим» в каком-либо материальном смысле, они просто разные. Все они легко потребляются, все они дают один и тот же результат, и ни один из них вряд ли окажет какое-либо реальное влияние на успех вашего проекта.

Рекомендации

  1. Переполнение стека: лучшие практики для управления версиями API? (отличный вопрос, отличные ответы, закрытые как «неконструктивные», я полагаю, потому что «Билл Ящерица» встал не с той стороны кровати тем утром)
  2. Блог Lexical Scope: Как работают REST API? (Хорошее сравнение методов управления версиями между сервисами. Хотя сейчас и пару лет)
  3. CodePlex: пример ограничения маршрутизации (связанный со страницей веб-API Microsoft в качестве примера API управления версиями путем добавления настраиваемого заголовка)
  4. CodeBetter.com: Управление версиями RESTful Services (очень прагматично и хорошее описание различных способов изменения API)
  5. Блог Vinay Sahni: лучшие практики для разработки прагматического API RESTful (он выступает за версионирование URL-адресов ради «возможности браузеров»)
  6. Pivotal Lans: управление версиями API (хороший взгляд на противоречивые мнения)
  7. Web Stack of Love: управление версиями ASP.NET Web API с типами мультимедиа (хорошее сквозное руководство по созданию приложения для поддержки контроля версий путем согласования содержимого)