Предисловие
За время многолетнего опыта в IT у многих из Вас были мысли или даже опыт создания своего API, в которое было бы заключено все то самое полезное и ценное, а в определенный момент пазл складывается и начинается этап выращивания и поедания собственного кактуса.
После года обкатки, перед дальнейшей модификацией и развитием, хочу поделиться получившимся функционалом сервиса определения мобильного оператора по номеру телефона (только РФ), подвергнуть само API и сервис Вашей критике :)
В статье есть примеры на PHP для обкатки на своей стороне для тех, кому он будет полезен в обмен на ценные замечания ;-)
Волшебный пендель в лице отмены мобильного рабства
Еще во времена 2003-2004 года, как сейчас помню, клиент пришел заполнять бумажный бланк для пополнения счета телефона. В графе оператора пользователь указал Билайн, а до боли знакомый код 916 явно мне подсказывал, что это МТС и платеж не будет благословлен, на что клиент меня успокоил: «Этот номер перенесен к другому оператору» — сказал он.
Спустя 10 лет эта процедура была уже законодательно утверждена и, наконец, заработала, только IT-специалисты получили небольшое увеличение нагрузки в виде подготовки систем к распознаванию операторов по номеру с учетом этой переносимости номеров от оператора к оператору — MNP.
Появление данных
Как водится, у всего централизованного есть единый реестр. В данном случае их было два: один — с редко изменяемыми данными — они определяли принадлежность номеров операторам большими блоками, например, 916 ХХХ-ХХ-ХХ. Второй — с динамическими, теми самыми перенесенными номерами — то есть каждый номер был отдельной записью.
Опустим технические детали. Просто был большой пласт, поверх которого «тусили» отдельные номера — вначале лукапили оператора диапазона, затем смотрели, есть ли записи о переносе номера к другому оператору. Пласт обновлялся раз в месяц, отдельные номера — каждый час (по мере переноса номеров).
Хранение и обновление
Осознав, что подход нужен более капитальный, на коленке решено было не делать… Да и какая тут уже коленка — полноценные договора между юридическими лицами для получения доступа к динамической части базы, регламентированная синхронизация — это уже не пришить к каждому отдельному проекту, который использовал эти данные — громоздко. Явно просматривался центр — место хранения данных.
Появление центра — API
Набросать первые пару скриптиков, которые бы отдавали данные — дело то не хитрое. Но одно дело делать для себя, другое — для продуктов, которые находились на стороне заказчика. Тем более, в воздухе витала идея полезного приложения для Android, а это уже пахло доступом для внешнего мира, возможные атаки с целью «сломать» приложение, да и просто школота, порой, из любознательности и для самоутверждения любит что-то поломать. Поэтому, внезапно появляется явный FRONT-END, AAA, BACK-END и обработчики запросов.
Видимые требования:
- Безопасная передача данных по открытым сетям
- Отказоустойчивость
- Стойкость к атакам от неавторизованных пользователей
- Возможность обращения простейшим методом HTTP GET
- Выдача в формате JSON
«Привет, Мир!» — самое сложное на самом простом примере
Вот здесь самое интересное. Обращение к API начинается с авторизации — получения токена для дальнейшего взаимодействия с системой. В качестве входных данных используются аж три идентифицирующих значения — имя пользователя, телефон, e-mail + пароль.
- корпоративный e-mail не дали унести с прежнего места работы
- телефон бесконтрольно ушел в минус во время командировки и номер отнял оператор
- пароль скомпрометирован
вот и получается, что неизменные только имя пользователя, да и то «пока», мне кажется. А все остальное очень даже непостоянно… А поменять один реквизит при наличии двух других — не проблема и вполне эквивалентно авторизации в несколько этапов (двухфакторная).
На одну учетную запись выдается только один токен в единицу времени. Его можно сменить, что вызовет «отзыв» полномочий у скриптов и приложений, которые под ним работали. Это и хорошо и плохо, но главное — контролируемо.
Представьте, что посетитель сайта хочет воспользоваться каким-то функционалом сервиса на стороннем сайте — сам сайт запросит для него новую учетку, авторизует (=получит токен), сообщит в параметрах сессий веб-браузера токен для взаимодействия с сервисом. Средствами браузера пользователь сам сможет совершать запросы, но у вас останется «руль» сессий не только внутри вашего портала, но и доступ к внешнему API вы легко закроете сменив токен, если сочтете, что доступ к информации пользователь более не должен получать.
Но прежде, мы сходим и стянем публичный ключик для взаимодействия поверх небезопасных сетей без SSL и следом подготовим данные.
$rsa_key = file_get_contents ('http://core.api.netresult.ru/rsa_public.pem');
$client_secret = md5(time());
$data = json_encode(
array ('request_type' => 'auth',
'username' =>'habr_demo_20160426',
'email' => 'support@media24-corp.ru',
'phone' => '74995799366',
'password' => md5('megapass'),
'client_secret' => $client_secret)
);
$pk = openssl_get_publickey($rsa_key);
openssl_public_encrypt($data, $encrypted, $pk);
$data = array('body' => chunk_split(base64_encode($encrypted)));
Вот такой кошмар ради простого запроса — эта куча манипуляций призвана предостеречь от изменения данных при передаче по каналам связи и для легкой валидации на FRONT-END'e.
function api_build_url (
$scheme='https', // протокол - http|https
$domain='core.api.netresult.ru', // домен API, общий публичный - core.api.netresult.ru
$validation_string_prefix='s', // строка без слешей, может быть произвольной (в соответствии с настройками)
$validation_secret='earth', // ключик для проверка запроса, прилетающего на FRONT-END - проверяется FRONT-END`ом в составе всего URI (строка добавленная к URI)
$data='', // POST данные. Данные ожидаются в переменной $data['body']
$api_version='3', // версия API, к которй будет совершаться запрос
$sla=1, // уровень SLA. Грубо - приоритет. Чем выше, тем приоритетнее. Уже сейчас можно использовать. Если при авторизации не возвращен - принять за единицу, в ином случае использовать приоритет, возвращенный при авторизации.
$uid=0, // идентификатор пользователя по базе API (AAA). 0 при авторизации, далее использовать возвращенный UID
$srv_name='AUTHENTICATE', // сервис, к которому происходит обращение со всеми параметрами этого сервиса
$client_secret // Секретный ключик клиента, сгенерированный на этапе авторизации. Передается серверу один раз, далее используется API для проверки подленност запроса.
) {
$url_template = $scheme . '://' . $domain . '/' . $validation_string_prefix . '/__NGINX_SIGN__/__QUERY_STRING__';
$request_string = 'v' . $api_version . '-sla' . $sla . '-uid' . $uid . '-sign__SOMESIGN__-req' . $srv_name;
$request_string = str_replace ('__SOMESIGN__', $client_secret, $request_string);
$request_string .= '=ts:' . time();
echo "request_string before md5: " . $request_string . PHP_EOL;
if (isset($data['body']) === true) {
$request_string .= '=postmd5:' . md5($data['body']);
}
$request_string = str_replace($client_secret, md5($request_string), $request_string);
$url = str_replace ('__QUERY_STRING__', $request_string, $url_template);
$request_string .= $validation_secret;
$query_sign = md5($request_string);
$url = str_replace ('__NGINX_SIGN__', $query_sign, $url);
return $url;
}
Наконец, добрались до авторизации — получаем токен и свой UID. Эту операцию можно сделать единожды за все-все время существования учетной записи, если нет нужды менять секрет/токен.
$url = api_build_url ('http', 'core.api.netresult.ru', 's', 'earth', $data, '3', 1, 0, 'AUTHENTICATE', $client_secret);
$options = array(
'http' => array(
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data),
),
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
print_r ($result);
/*
secret: 50c927316191f8678cec3b5247d1b34f
request_string before md5: v3-sla1-uid0-sign50c927316191f8678cec3b5247d1b34f-reqAUTHENTICATE=ts:1461624725
{"response":{"code":"200","data":{"uid":"36","sla":"1"},"msg":"authenticated"}}
*/
Далее учетные данные не используются — только $client_secret для подписания. Кстати, сразу отвечу на вопрос — да, можно в параметрах функции указать https для взаимодействия с использованием SSL, если запросы редкие и нет цели экономит ресурсы (или если они расходуются на стороне браузера клиента. Но понятно, что мы не отдаем клиенту функцию по авторизации, а вот запросы с токеном клиент может сам от себя выполнять по SSL при необходимости.).
Итак, авторизовались. Теперь присваиваем себе правильные UID и SLA, выполняем тестовый запрос.
$uid = $result['response']['data']['uid'];
$sla = $result['response']['data']['sla'];
echo PHP_EOL . PHP_EOL . "TEST SERVICE:" . PHP_EOL;
$url = api_build_url ('http', 'core.api.netresult.ru', 's', 'earth', '', '3', $sla, $uid, 'TEST', $client_secret);
$options = array(
'http' => array(
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'GET'
),
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
print_r ($result);
/*
TEST SERVICE:
request_string before md5: v3-sla1-uid36-sign50c927316191f8678cec3b5247d1b34f-reqTEST=ts:1461624725
{"response":{"code":"200","data":{"id":"21967","sla":"1","uid":"36","sign":"9cf6cba26113c52abea3af928f6232b6","srv":"test","req_head":"TEST=ts:1461624725","req_body":"","api_version":"3"},"msg":"ok"}}
*/
Ага, «ок»!
Извлекаем пользу
По аналогии обратимся к более полезному сервису LOOKUP_BY_NUMBER, для которого потребуются дополнительные параметры:
number - Номер телефона в международном формате (со знаком "+")
source - выбор источника (all - передавать по умолчанию),
и могут быть установлены дополнительные поля при необходимости (в мобильном приложении, например, для идентификации самих устройств и правильного формирования ответа)v - версия клиента
did - идентификатор устройства
loc - локализация
format - формат возвращаемых значений
echo PHP_EOL . PHP_EOL . "LOOKUP_BY_NUMBER SERVICE:" . PHP_EOL;
$url = api_build_url ('http', 'core.api.netresult.ru', 's', 'earth', '', '3', $sla, $uid, 'LOOKUP_BY_NUMBER'.'=number:+79671286464=v:6=loc:ru_RU=did:000000000000000000000000=format:no=source:all', $client_secret);
$options = array(
'http' => array(
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'GET'
),
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
print_r ($result);
/*
LOOKUP_BY_NUMBER SERVICE:
request_string before md5: v3-sla1-uid36-sign0e70a22605e5a178b572f0bfe426d210-reqLOOKUP_BY_NUMBER=number:+79671286464=v:6=loc:ru_RU=did:000000000000000000000000=format:no=source:all=ts:1461625693
{"response":{"code":200,"data":{"input":{"number":{"original":"+79671286464","parsed":"79671286464"}},"route":{"default":{"ServiceProvider_id":"3425721","ServiceProvider_name":"ОАО \"Вымпел-Коммуникации\"","ServiceProvider_region":"г. Москва и Московская область"},"mnp":{"ServiceProvider_id":2,"ServiceProvider_name":"\"Мобильные ТелеСистемы\" ПАО","ServiceProvider_region":"г. Москва и Московская область"}},"active_route":{"type":"mnp"}},"msg":""}}
*/
Возвращаемые значения
Результатом выполнения запроса будет являться таблица маршрутизации в порядке приоритета. Первая запись источника — активная. Остальные — просто есть в базе, но скорее всего устаревшие. Вывод зависит от соответствующей настройки «format».
ВАЖНО: ID Сервис провайдера возвращаются внутренние для каждого источника данных! Можно безопасно консолидировать по ID внутри одного источника.
Пример вывода:
{
"response": {
"code": 200,
"data": {
"input": {
"number": {
"original": "+79781234567",
"parsed": "79781234567"
}
},
"route": {
"default": {
"ServiceProvider_id": "274959",
"ServiceProvider_name": "Мобильные ТелеСистемы",
"ServiceProvider_region": "Краснодарский край"
}
},
"active_route": "default"
},
"msg": ""
}
}
Методы отправки данных
Можно использовать простейший метод GET, пример:
http://core.api.netresult.ru/s/f62bb062e30b01e7a689c8cdfdf10577/v3-sla1-uid0-signbb52d15783f7ed30df5d0e7a618cc048-reqLOOKUP_BY_NUMBER=number:+79781234567=v:6=loc:ru_RU=did:000000000000000000000000=format:no=source:all
а можно отправлять значения переменных и в POST-данных с шифрованием по примеру авторизации выше.
Пример работающего на базе API мобильного приложения для Android с минимальным функционалом самого приложения.
.
Комментарии и отзывы
Очень признателен за любую критику в отношении всего-всего, что сочтете нужным прокомментировать!
Комментарии (5)
FFAMax
26.04.2016 16:08-1В соседней статье «OAuth 2.0 простым и понятным языком» — https://habrahabr.ru/company/mailru/blog/115163/
одним из минусов явно выделено:
«Безопасность OAuth 2.0 во многом основана на SSL. Это сильно упрощает жизнь разработчикам, но требует дополнительных вычислительных ресурсов и администрирования. Это может быть существенным вопросом в высоко нагруженных проектах.»
На протяжении года как минимум один запрос разработчика был удовлетворен — допилили метод авторизации без подписи RSA-ключиком. Судя по тому, что это таки реализовали, основания были весомые — вероятно, где-то не было библиотек или ресурсов.
Еще один интересный пример из свежих — небрежность работы с SSL, как в примере с «отечественной разработкой `Ревизор`» — https://habrahabr.ru/post/282087/
Повсеместно SSL доберется еще не скоро…
antirek
27.04.2016 08:22блин, как вы живете с таким кодом?
функция api_build_url имеет больше 3-х параметров? Передавайте ассоциативный массив, который будет заполнен частично. Дефолтные значения подставите в самой функции.
конкатенация строк через точку? если не хотите понимать спецификаторы в sprintf, то хотя бы в массив через запятую, а затем join.
str_replace? все таки бы глянули sprintf: )
altervision
27.04.2016 10:08Подскажите, пожалуйста, откуда выгружаются сами реестры?
Основную базу номеров мы всегда выгружали из открытых данных Россвязи: www.rossvyaz.ru/opendata — а вот базы MNP видеть ещё не доводилось. Буду очень признателен за «наводку» ;)FFAMax
27.04.2016 10:10Ведение реестра возложено на ФГУП ЦНИИС, см. http://www.zniis.ru/bdpn/check
и еще по теме https://habrahabr.ru/post/216537/
napa3um
Необходимость работы без SSL притянута за уши, чтобы объяснить весь остальной энтузиазм велосипедостроения. В грядущем HTTP 2 в принципе не будет возможности работать без SSL. Смешиваете сессии с токенами (токены как раз и придумали для избавления от сессий, мешающих децентрализации сервисов). Для авторизации/аутентификации используйте схему OAuth 2, для неё уже триллион отлаженных библиотек под любые платформы. Через GET не стоит изменять данные на сервере, это противоречит принципам REST.