В продолжение моего предыдущего поста о таком интересном инструменте как Yiinitializr, я решил ответить на вопрос о возможностях работы API, предоставляемых шаблоном Advanced. В рамках комментария или дополнительного пункта к прошлой статье материал уместить не удалось, поэтому всех, кого интересует данная тема, приглашаю под кат. В ней мы не будем касаться принципов проектирования правильной архитектуры API, а разберёмся как воспользоваться трудами ребят из 2amigos, которые дали нам возможность быстро (после прочтения статьи — точно быстро) развернуть API для наших проектов на Yii.
Способ реализации работы с API в Yiinitializr
API — программный интерфейс приложения, служащий для использования во внешних программных продуктах. Если мы хотим, чтобы возможностями нашего приложения могли воспользоваться другие разработчики в своих проектах, то без хорошо спроектированного API нам не обойтись. К сожалению, Yii первой версии не сможет помочь в этом деле. Вероятно, вам подойдет Yiinitializr, который решит часть вопросов, но, как мы знаем, отсутствие документации является серьезным препятствием.
Представим, что работа над нашим замечательным приложением закончена, работа API налажена, и уже появился первый разработчик, желающий воспользоваться возможностями нашей системы. По какому принципу будет строиться её использование?
Наша система генерирует, выдаёт и сохраняет в базе данных публичный ключ (идентификатор внешнего приложения), приватный ключ, а также пользователя, за которым эти ключи резервируются. Регистрация на этом закончена. Взаимодействие пользователя API с нашей системой производится на основе принципов REST. Приложение пользователя отправляет нашей системе запрос определённым HTTP-методом, включающий в себя HTTP-заголовок с публичным ключом, а также сообщение в JSON-формате, в котором обязательно содержатся подпись и срок её годности, а также различные дополнительные параметры. Обработав запрос и убедившись в его корректности, система выдаёт ответ.
Отличия шаблона Advanced
Если раньше речь шла о шаблоне Intermediate, то теперь давайте взглянем, что же добавилось в шаблон Advanced. Как вы уже поняли — все дополнительные возможности, которые он нам даёт, связаны с API. Скачиваем, распаковываем и заходим в директорию ./api
смотреть, что же у нас теперь имеется. А имеются у нас:
./api/extensions/filters/EApiAccessControlFilter.php
— класс-фильтр для выполнения проверки правил доступа к API.
./api/extensions/components/EApiAccessRule.php
— класс, представляющий правило доступа к API.
./api/extensions/components/EApiActiveRecord.php
— класс для вспомогательных методов работы AR-моделей с API.
./api/extensions/components/EApiController.php
— класс-контроллер для обработки запросов к API.
./api/extensions/components/EApiError.php
— класс ошибок API, существующий для удобства чтения логов.
./api/extensions/components/EApiErrorHandler.php
— класс для обработки ошибок API. Если мы решим логировать ошибки в базу данных, то воспользуемся именно этим классом.
./api/models/ApiUser.php
— пример модели, которой мы будем управлять внешними пользователями нашего API.
./common/lib/YiiRestTools/
— вспомогательные классы для функционирования REST API.
Этих общих сведений будет достаточно для того, чтобы перейти непосредственно к реализации взаимодействия стороннего приложения с нашей системой посредством API.
Конфигурирование и исправление недоработок
Разработчики Yiinitializr по всей видимости придерживались принципа Парето, и выполнив 80% работы, решили не тратить 80% времени на документацию и исправление багов, возложив это на плечи искушённых разработчиков (то есть нас).
Раз мы решили заняться настройкой Yiinitializr, то конечно же нас интересует файл конфигурации. Откроем его и посмотрим на правила роутинга API (./api/config/api.php
):
'rules' => array( // REST patterns array('<controller>/index', 'pattern' => 'api/<controller:\w+>', 'verb' => 'POST'), array('<controller>/view', 'pattern' => 'api/<controller:\w+>/view', 'verb' => 'POST'), array('<controller>/update', 'pattern' => 'api/<controller:\w+>/update', 'verb' => 'PUT'), array('<controller>/delete', 'pattern' => 'api/<controller:\w+>/delete', 'verb' => 'DELETE'), array('<controller>/create', 'pattern' => 'api/<controller:\w+>/create', 'verb' => 'POST'), ),
Видим что-то непонятное. Комментарий говорит нам, что это REST-шаблон, но на деле получаем не совсем то. REST предполагает, что все запросы идут на единый URL, а действия выбираются на основе HTTP-методов и параметров запроса, т. е. должно быть так:
Адрес | HTTP-метод | Вызванное действие |
---|---|---|
api.yiinitializr.dev/test/ | GET | TestController\actionIndex() |
api.yiinitializr.dev/test/1/ | GET | TestController\actionView(1) |
api.yiinitializr.dev/test/ | POST | TestController\actionCreate() |
api.yiinitializr.dev/test/1/ | PUT | TestController\actionUpdate(1) |
api.yiinitializr.dev/test/1/ | DELETE | TestController\actionDelete(1) |
Приводим правила к нужному виду:
'rules' => array( // REST patterns array('<controller>/index', 'pattern' => '<controller:\w+>', 'verb' => 'GET'), array('<controller>/view', 'pattern' => '<controller:\w+>/<id:\d+>', 'verb' => 'GET'), array('<controller>/update', 'pattern' => '<controller:\w+>/<id:\d+>', 'verb' => 'PUT'), array('<controller>/delete', 'pattern' => '<controller:\w+>/<id:\d+>', 'verb' => 'DELETE'), array('<controller>/create', 'pattern' => '<controller:\w+>', 'verb' => 'POST'), ),
Больше в файле конфигурации ничего действительно важного для нынешнего этапа мы не найдём, поэтому переходим к другим проблемам. Решение первой абсолютно простое — переносим
use YiiRestTools\Helpers\RequestData; use Yiinitializr\Helpers\ArrayX;
из EApiAccessControlFilter.php
в EApiAccessRule.php
, т. к. эти классы используются именно во втором файле.
Следующая проблема уже интереснее. Возможно, я чего-то не понял, поэтому предлагаю порассуждать вместе. Внимательно посмотрите на приведённый ниже код (./api/extensions/components/EApiAccessRule.php
):
public function isRequestAllowed($user, $controller, $action, $ip, $verb) { if ($this->isActionMatched($action) && $this->isUserMatched(Yii::app()->user) && $this->isRoleMatched(Yii::app()->user) && $this->isSignatureMatched($user) && $this->isIpMatched($ip) && $this->isVerbMatched($verb) && $this->isControllerMatched($controller) ) { return $this->allow ? 1 : -1; } else { return 0; } }
Метод isRequestAllowed
проверяет соответствие запроса правилам. Если цепочка проверок в блоке if истинна, то данное правило применяется, возвращая 1 или -1, в зависимости от того, что делает это правило — разрешает или запрещает. Иначе данное правило не применимо к конкретному запросу и метод возвращает 0. Чтобы стало понятнее, напоминаю, как выглядят правила для фильтров:
public function filters() { return array( array( 'EApiAccessControlFilter -error', 'rules' => array( array('allow', 'users' => array('@')), ) ) ); }
Смущает одно, а именно проверка подписи $this->isSignatureMatched($user)
в этой цепочке. Получая неправильную подпись, система решает, что данное правило неприменимо и соответственно пропускает пользователя (или хакера) внутрь. Скорее всего проверка подписи должна производиться у корректного запроса уже после, и по результату впускать или не впускать нас в систему. Следовательно необходимо немного изменить данный метод:
public function isRequestAllowed($user, $controller, $action, $ip, $verb) { if ($this->isActionMatched($action) && $this->isUserMatched(Yii::app()->user) && $this->isRoleMatched(Yii::app()->user) && $this->isIpMatched($ip) && $this->isVerbMatched($verb) && $this->isControllerMatched($controller) ) { return ($this->allow && $this->isSignatureMatched($user)) ? 1 : -1; } else { return 0; } }
С недоработками вроде покончено. В игру вступает отсутствие документации. С помощью дебаггера по шагам я изучил механизмы взаимодействия API-клaссов и спешу поделиться наблюдениями с вами.
Для начала проделайте основные настройки, как описано в Большом руководстве. Затем давайте определим в конфигурации название HTTP-заголовка, в котором мы будем отправлять публичный ключ (./api/config/api.php
):
'params' => array( 'api.key.name' => 'APIKEY', )
Не забываем выставить правильный часовой пояс (он должен быть одинаковым у нашей системы и у клиентского приложения) (common/config/main.php
):
'params' => array( ... 'php.timezone' => 'Europe/Moscow', ),
Запускаем установку через Composer, уже можно.
Теперь, чтобы провести тестирование API, нам необходимо зарегистрировать внешнего пользователя. Создаём новую миграцию:
> yiic migrate create create_api_user_table
И приводим методы up()
и down()
к следующему виду:
public function up() { $this->createTable('{{api_user}}', array( 'id' => 'pk', 'username' => 'varchar(32) NOT NULL', 'api_key' => 'varchar(32) NOT NULL', 'api_secret' => 'varchar(32) NOT NULL', )); $this->insert('{{api_user}}', array( 'username' => 'test_user', 'api_key' => 'e4afe26b5b57083f74b2d01c7066379c', // md5('public_key') 'api_secret' => '156a17333e77a3c504018cae5ada8c3b', // md5('private_key') )); } public function down() { $this->dropTable('{{api_user}}'); }
Также подредактируем название таблицы, содержащей пользователей нашего API, в модели ApiUser
.
class ApiUser extends EApiActiveRecord { ... public function tableName() { return '{{api_user}}'; } ... }
Применяем нашу миграцию. Результатом станет таблица в базе данных с единственным гипотетическим пользователем нашего API. Идём дальше.
Пишем простое клиентское приложение
Последним шагом станет написание простого клиентского приложения для работы с API Yiinitializr. Для его работы необходимо иметь возможность отправлять запросы различными HTTP-методами, в этом нам поможет библиотека cURL. Без лишних слов весь код разложен под спойлеры. Открываем, смотрим, копируем.
generateSignature()
для генерации подписи основывается на методе prepareData($secretKey)
класса RequestData
библиотеки YiiRestTools
.
/** * Class SimpleClient * * Simple REST-client for Yiinitializr Advanced API. */ class SimpleClient { private $baseUrl; private $apiPublic; private $apiSecret; private $expiration; public function __construct($url, $publicKey, $secretKey, $expiration = '+1 hour') { $this->baseUrl = $url; $this->apiPublic = $publicKey; $this->apiSecret = $secretKey; $this->expiration = $expiration; } public function makeRequest($verb, $controller, $params = array()) { $ch = curl_init(); $signature = $this->generateSignature(); $url = $this->makeUrl($controller); if (!empty($params) && isset($params['id'])) { $url .= $params['id']; } curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_CUSTOMREQUEST => $verb, CURLOPT_HTTPHEADER => array('APIKEY: ' . $this->apiPublic), CURLOPT_POSTFIELDS => json_encode(array( 'signature' => $signature, 'expiration' => $this->relativeTimeToAbsolute($this->expiration), )), )); $result = curl_exec($ch); curl_close($ch); return $result; } private function generateSignature() { $ttdInt = strtotime($this->expiration); $raw = json_encode(array('expiration' => gmdate('Y-m-d\TH:i:s\Z', $ttdInt))); $jsonPolicy64 = base64_encode($raw); $signature = base64_encode(hash_hmac( 'sha1', $jsonPolicy64, $this->apiSecret, true )); return $signature; } private function makeUrl($controller) { return 'http://' . rtrim($this->baseUrl, '/') . '/' . $controller . '/'; } private function relativeTimeToAbsolute ($relativeTime) { return date('M d Y, H:i:s', strtotime($relativeTime)); } }
$api = new SimpleClient('api.yiinitializr.dev', 'e4afe26b5b57083f74b2d01c7066379c', '156a17333e77a3c504018cae5ada8c3b'); $api->makeRequest('GET', 'test'); $api->makeRequest('GET', 'test', array('id' => 1)); $api->makeRequest('POST', 'test'); $api->makeRequest('PUT', 'test', array('id' => 1)); $api->makeRequest('DELETE', 'test', array('id' => 1));
class TestController extends EApiController { public function actionIndex() { // just drop API request :) $this->renderJson(json_encode(array('response' => 'index'))); } public function actionView($id) { $this->renderJson(json_encode(array('response' => 'viewed#' . $id))); } public function actionCreate() { $this->renderJson(json_encode(array('response' => 'created'))); } public function actionUpdate($id) { $this->renderJson(json_encode(array('response' => 'updated#' . $id))); } public function actionDelete($id) { $this->renderJson(json_encode(array('response' => 'deleted#' . $id))); } }
./api/www/.htaccess
следующие строки:
<Limit GET POST PUT DELETE> order deny,allow allow from all </Limit>
Подводим итоги
Вот таким нехитрым способом мы заставили нашу машину завестись. На то, чтобы понять что к чему, мне понадобилось потратить не один день (и даже не два). В любом случае, подобное решение будет вполне уместно в качестве стартовой позиции, основываясь на которой гораздо легче сделать качественное API, не обладая глубокими знаниями в этом вопросе. Из дополнительных ссылок могу посоветовать посмотреть Большое руководство по Yiinitializr (неужели вы до сих пор этого не сделали?) и статью Как сделать REST API для Yii (на английском).
В комментариях предлагаю поделиться вашими соображениями по поводу реализации API на Yii, покритиковать данный способ и предложить улучшения. Спасибо за внимание.
ссылка на оригинал статьи http://habrahabr.ru/post/208376/
Добавить комментарий