Проектирование API для людей: Идентификаторы объектов


Выбор типа идентификатора

Независимо от того, каким видом бизнеса вы занимаетесь, вам, скорее всего, нужна база данных для хранения важных данных, таких как информация о клиентах или статус заказов. Однако хранение — это только одна часть работы; вам также необходим способ быстрого извлечения данных — именно здесь на помощь приходят идентификаторы.

Также известные как первичный ключ, идентификаторы используются для уникального определения строки в таблице. При проектировании таблицы вам нужна система, в которой идентификаторы легко генерируются, уникальны и читаемы человеком.

Самый простой подход к идентификаторам при использовании реляционной базы данных — это использование идентификатора строки, который представляет собой целое число. Идея заключается в том, что при добавлении новой строки (т.е. при создании нового клиента) ID будет следующим порядковым номером. Это звучит как хорошая идея, поскольку это позволяет легко обсуждать в разговоре («У заказа 56 есть проблемы, не могли бы вы взглянуть?»), плюс это не требует никакой работы по настройке. Однако на практике это кошмар безопасности, который только и ждет своего часа. Использование целочисленных идентификаторов делает вас широко открытым для атак перечисления, когда злоумышленникам становится тривиально легко угадать идентификаторы, которые они не должны иметь возможности, поскольку ваши идентификаторы являются последовательными.

Например, если я зарегистрируюсь на вашем сервисе и узнаю, что мой ID пользователя «42», то я могу сделать предположение, что существует пользователь с ID «41». Вооружившись этим знанием, я могу получить конфиденциальные данные о пользователе «41», к которым меня абсолютно не должны допускать, например, незащищенную конечную точку API, такую как /api/customers/:id/. Если ID является чем-то, что я не могу угадать, то эксплуатировать эту конечную точку становится намного сложнее.

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

Вместо этого вам нужно убедиться, что ваши идентификаторы уникальны и их невозможно угадать.

Гораздо лучшим кандидатом на роль идентификатора является Универсальный уникальный идентификатор, или UUID. Это 32-значная смесь буквенно-цифровых символов (и поэтому хранится как строка). Вот пример такого идентификатора:

4c4a82ed-a3e1-4c56-aa0a-26962ddd0425

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

С другой стороны, вот идентификатор объекта Stripe:

pi_3LKQhvGUcADgqoEM3bh6pslE

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

Сделайте его человекочитаемым

pi_3LKQhvGUcADgqoEM3bh6pslE
└─┘└──────────────────────┘
 └─ Prefix    └─ Randomly generated characters
Вход в полноэкранный режим Выйти из полноэкранного режима

Вы могли заметить, что все объекты Stripe имеют префикс в начале идентификатора. Причина этого довольно проста: добавление префикса делает ID человекочитаемым. Не зная ничего другого об идентификаторе, мы можем сразу подтвердить, что речь идет об объекте PaymentIntent, благодаря префиксу pi_.

Когда вы создаете PaymentIntent через API, вы фактически создаете или ссылаетесь на несколько других объектов, включая Customer (cus_), PaymentMethod (pm_) и Charge (ch_). С помощью префиксов вы можете сразу различать все эти различные объекты с первого взгляда:

$pi = $stripe->paymentIntents->create([
  'amount' => 1000,
  'currency' => 'usd',
  'customer' => 'cus_MJA953cFzEuO1z',
  'payment_method' => 'pm_1LaXpKGUcADgqoEMl0Cx0Ygg',
]);
Войти в полноэкранный режим Выйти из полноэкранного режима

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

$pi = $stripe->paymentIntents->retrieve(
  $id,
  [],
  ['stripe_account' => 'cus_1KrJdMGUcADgqoEM']
);
Войти в полноэкранный режим Выйти из полноэкранного режима

Приведенный выше фрагмент пытается получить PaymentIntent из подключенного аккаунта, однако, даже не глядя на код, вы можете сразу обнаружить ошибку: вместо ID аккаунта (cus_) используется ID клиента (acct_). Без префиксов это было бы гораздо сложнее отлаживать; если бы Stripe использовала UUID вместо этого, нам пришлось бы искать ID (возможно, в Stripe Dashboard), чтобы узнать, что это за объект и действителен ли он вообще.

В Stripe мы пошли настолько далеко, что разработали внутреннее расширение браузера для автоматического поиска объектов Stripe на основе их ID. Поскольку мы можем определить тип объекта по префиксу, тройное нажатие на ID автоматически открывает соответствующую внутреннюю страницу, что значительно упрощает отладку.

Полиморфный поиск

Говоря о выводе типов объектов, следует отметить, что это особенно актуально при разработке API с учетом обратной совместимости.

При создании PaymentIntent вы можете опционально предоставить параметр payment_method, чтобы указать, какой тип платежного инструмента вы хотите использовать. Возможно, вы не знаете, что вместо идентификатора PaymentMethod (pm_) здесь можно указать идентификатор Source (src_) или Card (card_). PaymentMethods заменили Sources и Cards в качестве канонического способа представления платежного инструмента в Stripe, но по причинам обратной совместимости мы все еще должны иметь возможность поддерживать эти старые объекты.

$pi = $stripe->paymentIntents->create([
  'amount' => 1000,
  'currency' => 'usd',
  // This could be a PaymentMethod, Card or Source ID
  'payment_method' => 'card_1LaRQ7GUcADgqoEMV11wEUxU',
]);
Вход в полноэкранный режим Выход из полноэкранного режима

Без префиксов у нас не было бы возможности узнать, какой тип объекта представляет идентификатор, а значит, мы не знали бы, к какой таблице запрашивать данные объекта. Запрашивать все таблицы, чтобы найти один идентификатор, крайне неэффективно, поэтому нам нужен лучший метод. Одним из способов может быть требование дополнительного параметра «тип»:

$pi = $stripe->paymentIntents->create([
  'amount' => 1000,
  'currency' => 'usd',
  // Without prefixes, we'd have to supply a 'type'
  'payment_method' => [
    'type' => 'card',
    'id' => '1LaRQ7GUcADgqoEMV11wEUxU'
  ],
]);
Войти в полноэкранный режим Выход из полноэкранного режима

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

С помощью префикса мы можем сразу сделать вывод о том, является ли платежный инструмент одним из PaymentMethod, Source или Card, и знать, к какой таблице нужно сделать запрос, несмотря на то, что это совершенно разные типы объектов.

Предотвращение человеческих ошибок

Есть и другие, менее очевидные преимущества префиксации. Одно из них — простота работы с идентификаторами, когда вы можете определить их тип по первым нескольким символам. Например, на сервере Stripe Discord мы используем функцию AutoMod в Discord, чтобы автоматически отмечать и блокировать сообщения, содержащие секретный ключ API Stripe live, который начинается с sk_live_. Утечка такого секретного ключа может иметь серьезные последствия для вашего бизнеса, поэтому мы принимаем меры, чтобы этого не произошло в средах, которые мы контролируем.

Благодаря тому, что ключи начинаются с sk_live_, написание regex для отсеивания случайных утечек не составляет труда:

Таким образом мы можем предотвратить утечку секретных живых API-ключей в Discord, но разрешить публикацию тестовых ключей в формате sk_test_123 (хотя их тоже следует держать в секрете).

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

if (preg_match("/sk_live/i", $_ENV["STRIPE_SECRET_API_KEY"])) {
  echo "Live key detected! Aborting!";
  return;
}

echo "Proceeding in test mode";
Вход в полноэкранный режим Выход из полноэкранного режима

Stripe использует эту технику префиксации с 2012 года, и, насколько я знаю, мы первые, кто реализовал ее в масштабе. (Это неверно? Дайте мне знать в комментариях ниже!). До 2012 года все идентификаторы объектов в Stripe больше походили на традиционные UUID. Если вы были одним из первых пользователей Stripe, вы можете заметить, что идентификатор вашей учетной записи все еще выглядит так, без префикса.

Редактирование: IETF опередила Stripe на несколько лет, разработав спецификацию URN. Используете ли вы формат URN в своей работе? Дайте мне знать!

Проектирование API для людей

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

Надеюсь, эта статья убедила вас в преимуществах префиксации ваших идентификаторов. Если вам интересно, как их эффективно реализовать (и вы работаете на Ruby), Крис Оливер создал гем, который делает добавление этого в ваши системы тривиальным.

Об авторе

Пол Асджес — представитель разработчиков в Stripe, где он пишет, кодирует и ведет ежемесячную серию Q&A, общаясь с разработчиками. Вне работы он любит варить пиво, готовить билтонг и проигрывать своему сыну в Mario Kart.

Оцените статью
devanswers.ru
Добавить комментарий