Создаём собственный API-6
Всем привет. Сегодня мы разработаем два ключевых класса. Во-первых, мы изменим способ обращения к Gateway и во-вторых, разработаем Api Handler который сможет быть по настоящему классом-контейнером для всей иерархии API.
Начнём с API Handler, чтобы изменения в Gateway были более очевидны. Забегая вперёд, заметим, что все изменения не касаются удалённого хоста. Т.о. драйвер для работы с API на удалённом хосте остаётся таким же. На API хосте мы всё так же принимаем запрос и всё так же возвращаем ответ, но, в данном случае, меняется способ обработки этого запроса и формат ответа в случае ошибки. Единственные изменения, которые должны произойти на удалённом хосте – это информация об авторизации (имя пользователя и пароль), которую необходимо добавить к запросу.
class api_handler extends api { //три объекта, которыми мы будем манипулировать. protected $api_gateway = null; protected $api_login = null; protected $api_access = null; //запрос от удалённого хоста protected $request_data = array(); //данные об удалённом хосте (авторизация) protected $user_data = array(); //в конструкторе создаём необходимые объекты public function __construct($db_object) { parent::__construct($db_object); //этот класс мы рассмотрим чуть ниже $this->api_gateway = new api_gateway_smart($this->db_handler); $this->api_login = new api_login($this->db_handler); $this->api_access = new api_access($this->db_handler); } //исполняемая функция array main(array) принимает запрос //от удалённого хоста, //возвращает массив с ответом public function main($request) { //получили данные по установленному протоколу и привели их в рабочий вид $this->request_data = $this->api_gateway->get_request($request); //если данные совпадают с ожидаемыми if($this->request_data) { //проходим авторизацию. $this->user_data = $this->api_login->autorize( $this->request_data['username'], $this->request_data['password']); //если авторизация пройдена if($this->user_data) { //и уровня доступа хватает для вызова запрошенной функции if($this->api_access->chk_access( $this->user_data['access'], $this->request_data['f_name'])) { //вызываем API функцию через объект шлюза $this->api_gateway->process_data(); //и возвращаем результат её работы return $this->api_gateway->send_response(); } else { //если не хватает уровня доступа для вызова API функции, //возвращаем ошибку return $this->api_gateway->send_response(false, 3); } } else { //если не пройдена авторизация, возвращаем ошибку return $this->api_gateway->send_response(false, 2); } } else { //если запрос не соответствует протоколу, возвращаем ошибку return $this->api_gateway->send_response(false, 1); } } }
Теперь изменим поведение класса Gateway для лучшей работы Api_Handler.
interface api_gateway_smart_itrfc { // array send_response([bool] [, integer]]) возвращаем // ответ удалённому хосту. // в качестве необязательных параметров получает флаг в случае ошибки // и код ошибки public function send_response($flag=true, $int=0); //единственные изменения, которые произошли в этом методе - тип доступа. //был protected стал public public function process_data(); } class api_gateway_smart extends api_gateway implements api_gateway_itrfc, api_gateway_smart_itrfc { //массив с ответом protected $response_data = array(); //в api_gateway я использовал эту переменную без объявления. //синтаксис это позволяет, но //правила хорошего тона - нет. так что прошу прощения за эту оплошность. protected $request_data = array(); //теперь мы будем жёстко проверять формат запроса и это будет ещё одной //дополнительной мерой безопасности protected $request_data_format = array(); public function __construct($db_object) { parent::__construct($db_object); // инициализируем переменную, отвечающую за формат запроса. // теперь мы точно знаем все ключи и типы значений в запросе. // если значений больше или меньше или если их типы // не совпадают с тем, что мы ожидаем, // то мы такой запрос будем игнорировать. $this->request_data_format['username'] = ''; $this->request_data_format['password'] = ''; $this->request_data_format['f_name'] = ''; $this->request_data_format['args'] = array(); } public function get_request($str) { //протокол parent::prepare_data($str); if($this->chk_request()) { //если проверка формата запроса успешно пройдена, работаем дальше return $this->request_data; } else { //если нет, то возвращаем ошибку return false; } } public function process_data() { return parent::process_data(); } public function send_response($bool=true, $int=0) { if($bool) { //если метод вызван с параметрами по умолчанию, то возвращаем //родительский send_response, а именно //отправляем заголовки удалённому хосту методом echo return parent::send_response(); } else { //в противном случае произошла какая-то ошибка. //первым делом, в целях безопасности, обнулим //переменную $this->response_data, //которая содержит информацию для удалённого хоста unset($this->response_data); //теперь сформируем массив с инфомацией об ошибке с учётом кода, //от вызывающего приложения //эту часть можно спрятать в базу данных или в конфигурационный файл switch($int) { case '1': $this->response_data = array( 'error'=>'unsupported request format', 'errno'=>1); break; case '2': $this->response_data = array( 'error'=>'autorization failed', 'errno'=>2); break; case '3': $this->response_data = array( 'error'=>'access denied', 'errno'=>3); break; default: break; $this->response_data = array( 'error'=>'unknown', 'errno'=>0); } //и вернём удалённому хосту return parent::send_response(); } } // bool chk_request(void) - один из ключевых методов, // отвечающих за безопасность. // возвращает true только в том случае, если запрос сформирован // действительно правильно и не содержит // потенциально опасных данных protected function chk_request() { $temp = array(); //временная переменная, //аналог $this->request_data //сначала убедися, что запрос - действительно массив if(is_array($this->request_data)) { //затем в цикле пройдёмся по эталонному формату запроса. //эта переменная определяется в конструкторе. foreach($this->request_data_format as $key => $val) { //сравним каждый элемент массива, полученный от удалённого хоста, //с элементом эталонного массива //на предмет совпадения ключа и типа if(array_key_exists($key, $this->request_data) && (gettype($val)===gettype($this->request_data[$key]))) { //если всё ок, то сохраним текущее значение во временной переменной $temp[$key] = $this->request_data[$key]; } else { //в противном случае, запрос полученный от удалённого хоста //не содержит обязательных данных //или содержит данные другого типа return false; } } //обнуляем $this->request_data и присваиваем ей значение //временной переменной - это гарантия того, что //в запросе от пользователя не пришло каких-то дополнительных //неожиданных данных, которые могут потенциально //угрожать безопасности unset($this->request_data); $this->request_data = $temp; //если мы добрались до этой точки, то //$this->request_data на 100% имеет правильный формат и //содержит все необходимые данные для правильной работы приложения return true; } else { return false; } } }
Таким образом, мы получили новый класс, наследник первой версии шлюза, который проверяет пользовательский запрос и гарантирует правильный формат данных для нормальной работы всего приложения. Теперь api_handler может свободно манипулировать всеми высокоуровневыми методами, оставляя внизу вопросы низкоуровневых проверок, выполнения запроса, реализацию протокола и т.п. По сути, api_handler стал удобной обёрткой. Его структура отличается от рассмотренной в первой статье. В первой статье предполагалось, что шлюз будет чем-то внешним, но сейчас api_handler реализован так, что он скрывает шлюз в себе. Хотя, логическая суть шлюза от этого не изменилась. Шлюз по прежнему принимает запрос и отправляет ответ удалённому хосту.
В следующей части статьи мы отфильтруем запрос удалённого хоста от потенциально опасного мусора и предотвратим различные неприятности в духе SQL-инъекций. Конечно, для этого можно было бы использовать какой-нибудь мощный класс типа AdoDB, в котором механизм защиты уже реализован, но, во-первых, этот пример API является исключительно демонстрационным, во-вторых, возможно, нам понадобится фильтровать запрос не только на уколы. Так что ждём следующей статьи, чтобы посмотреть на новые изменения 🙂
Как всегда, полная версия кода доступна в аттачменте, а я жду ваших критических замечаний и комментариев.
Всем удачи!
В тему: