Как я заставил работать API в Yiinitializr Advanced

от автора

В продолжение моего предыдущего поста о таком интересном инструменте как 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. Без лишних слов весь код разложен под спойлеры. Открываем, смотрим, копируем.

Класс клиента SimpleClient

Метод 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));     } } 

Работа с классом SimpleClient

$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)));     } } 

Настройка Apache

Чтобы Apache перестал блокировать PUT и DELETE запросы, необходимо добавить в файл ./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/


Комментарии

Добавить комментарий

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