Предисловие
За время многолетнего опыта в IT у многих из Вас были мысли или даже опыт создания своего API, в которое было бы заключено все то самое полезное и ценное, а в определенный момент пазл складывается и начинается этап выращивания и поедания собственного кактуса.
После года обкатки, перед дальнейшей модификацией и развитием, хочу поделиться получившимся функционалом сервиса определения мобильного оператора по номеру телефона (только РФ), подвергнуть само API и сервис Вашей критике 🙂
В статье есть примеры на PHP для обкатки на своей стороне для тех, кому он будет полезен в обмен на ценные замечания 😉
Волшебный пендель в лице отмены мобильного рабства
Еще во времена 2003-2004 года, как сейчас помню, клиент пришел заполнять бумажный бланк для пополнения счета телефона. В графе оператора пользователь указал Билайн, а до боли знакомый код 916 явно мне подсказывал, что это МТС и платеж не будет благословлен, на что клиент меня успокоил: «Этот номер перенесен к другому оператору» — сказал он.
Спустя 10 лет эта процедура была уже законодательно утверждена и, наконец, заработала, только IT-специалисты получили небольшое увеличение нагрузки в виде подготовки систем к распознаванию операторов по номеру с учетом этой переносимости номеров от оператора к оператору — MNP.
Появление данных
Как водится, у всего централизованного есть единый реестр. В данном случае их было два: один — с редко изменяемыми данными — они определяли принадлежность номеров операторам большими блоками, например, 916 ХХХ-ХХ-ХХ. Второй — с динамическими, теми самыми перенесенными номерами — то есть каждый номер был отдельной записью.
Опустим технические детали. Просто был большой пласт, поверх которого «тусили» отдельные номера — вначале лукапили оператора диапазона, затем смотрели, есть ли записи о переносе номера к другому оператору. Пласт обновлялся раз в месяц, отдельные номера — каждый час (по мере переноса номеров).
Хранение и обновление
Осознав, что подход нужен более капитальный, на коленке решено было не делать… Да и какая тут уже коленка — полноценные договора между юридическими лицами для получения доступа к динамической части базы, регламентированная синхронизация — это уже не пришить к каждому отдельному проекту, который использовал эти данные — громоздко. Явно просматривался центр — место хранения данных.
Появление центра — API
Набросать первые пару скриптиков, которые бы отдавали данные — дело то не хитрое. Но одно дело делать для себя, другое — для продуктов, которые находились на стороне заказчика. Тем более, в воздухе витала идея полезного приложения для Android, а это уже пахло доступом для внешнего мира, возможные атаки с целью «сломать» приложение, да и просто школота, порой, из любознательности и для самоутверждения любит что-то поломать. Поэтому, внезапно появляется явный FRONT-END, AAA, BACK-END и обработчики запросов.
«Привет, Мир!» — самое сложное на самом простом примере
Вот здесь самое интересное. Обращение к 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 с минимальным функционалом самого приложения.
ссылка на оригинал статьи https://habrahabr.ru/post/282443/
Добавить комментарий