Создаём собственный API-5
Ну что ж, пришло время поговорить о безопасности. Но прежде чем мы приступим, я немного вернусь назад. В самой первой статье я говорил о том, что информацию можно вернуть вызываемому приложению как объект. Как вернуть информацию в виде массива мы уже разобрались. Массив – это просто данные, которыми удобно манипулировать.
Если вы хотите предоставить удалённым разработчикам объект, который, в т.ч., может обращаться к Host 1 для вызова API, достаточно просто создать этот объект на стороне API-сервера и вернуть вызываемому приложению вместе с классом (и всеми родительскими классами), который является типом этого объекта. Сделать это очень просто. Достаточно прочитать файл, в котором хранится определение класса, с помощью стандартных функций типа fopen и вернуть в качестве строки. На стороне приложения (Host 2) эту строку нужно сохранить в локальный файл (фактически получим копию файла на API-сервере) и подключить этот файл с помощью incude.
О том, как передаются сериализованные объекты, можно подробнее прочитать в справке PHP.
Теперь вплотную займёмся безопасностью. Условно, меры, которые мы можем предпринять, можно разделить на следующие пункты:
- Авторизация с уровнями доступа.
- Фильтрация запроса.
- Использование защищённого соединения.
- Использование различных трюков.
В этой части статьи мы обсудим авторизацию и подготовимся к п.2. Что касается защищённого соединения – речь идёт о передаче данных по протоколу HTTPS, с которым cURL отлично работает.
Однако, можно продолжать передавать данные по HTTP, но добавить к нашему протоколу функцию шифрования. Это просто бездонная тема и останавливаться на этом мы не будем (возможно, мы рассмотрим некоторые идеи в других статьях, не касающихся API). В любом случае, относительно п.4., – я подкину вам пару идей, которые вы можете использовать в своих рабочих приложениях уже сейчас:
- В запросе передавать не имена функций, а их хэши.
- Клиент шифрует свой запрос функцией encrypt_foo_client и передаёт его API-серверу. API-сервер шифрует запрос функцией encrypt_foo_server и возвращает его API-клиенту. API-клиент расшифровывает запрос функйцией decrypt_foo_client и возвращает запрос серверу. Сервер расшифровывает запрос функцией decrypt_foo_server и приступает к работе. (см. подробнее симметричные/ассимитричные криптосистемы, криптосистемы с открытыми/закрытыми ключами)
- Использовать контрольную сумму, хэш случайной строки и т.п., которые будут каждый раз проверятся, чтобы установить подлинность запроса.
На самом деле, если речь идёт о деньгах, все эти танцы с бубном вполне оправданы. Но, как учит нас мистер Кевин Митник – какой бы сложной ни была система, критическая уязвимость – это человеческий фактор 🙂 Так что приступим к авторизации и оставим проблемы безопасности данных на совесть вашего бюджета.
Итак, нам понадобится ещё одна утилита для создания дополнительных таблиц в базе.
class api_access_utility extends api { // тут мы будем хранить типы доступа protected $api_access = 'api_access'; //промежуточная таблица для реализации отношения многие ко многим protected $api_access_links = 'api_access_links'; //пользователи API с уровнем привелегий protected $api_users = 'api_users'; protected $basic_queries = array(); protected $db_handler = null; public function __construct($db_object) //так же как в прошлый раз { parent::__construct($db_object); $this->create_queries(); } //создаём массив запросов к базе protected function create_queries() { $this->basic_queries['tables'][] = 'DROP TABLE IF EXISTS `'. $this->api_access.'`'; $this->basic_queries['tables'][] = 'CREATE TABLE `'. $this->api_access.'`(' . 'id INT not null auto_increment primary key, ' . 'role_name char(255) binary not null, access tinyint not null)'; $this->basic_queries['tables'][] = 'DROP TABLE IF EXISTS `'. $this->api_users.'`'; $this->basic_queries['tables'][] = 'CREATE TABLE `'. $this->api_users.'`(' . 'id INT not null auto_increment primary key, ' . 'access tinyint not null, uname char(255) binary not null, ' . 'pwd char(255) binary not null)'; $this->basic_queries['tables'][] = 'DROP TABLE IF EXISTS `'. $this->api_access_links.'`'; $this->basic_queries['tables'][] = 'CREATE TABLE `'. $this->api_access_links.'`(' . 'id INT not null auto_increment primary key, ' . 'api_id int not null, access_id tinyint not null)'; $this->basic_queries['access'][] = 'INSERT INTO `'. $this->api_access.'` values (NULL, "select", 1)'; $this->basic_queries['access'][] = 'INSERT INTO `'. $this->api_access.'` values (NULL, "insert", 2)'; $this->basic_queries['access'][] = 'INSERT INTO `'. $this->api_access.'` values (NULL, "delete", 4)'; // обратите внимание - в таблице пользователей второе значение - // это уровень доступа. мы будем использовать битовый доступ // по степеням двойки. таким образом, select будет равен 1 // insert будет равен 3 (1+2 т.к. включает в себя операции select) // и delete будет равен 7 (1+2+4 = select + insert + delete) $this->basic_queries['users'][] = 'INSERT INTO `'. $this->api_users.'` values (NULL, 1, "user1", "'.md5('pwd1').'")'; $this->basic_queries['users'][] = 'INSERT INTO `'. $this->api_users.'` values (NULL, 1+2, "user2", "'.md5('pwd2').'")'; $this->basic_queries['users'][] = 'INSERT INTO `'. $this->api_users.'` values (NULL, 1+2+4, "user3", "'. md5('pwd3').'")'; $this->basic_queries['links'][] = 'INSERT INTO `'. $this->api_access_links.'` values (NULL, "?", "!")'; return; } public function run() //исполняемая функция { $info = array(); $data = array(); $data = $this->db_handler->res_query('select * from `'. $this->api_table.'`'); //создаём таблицы в базе for($i=0; $ibasic_queries['tables']); $i++) { $this->db_handler->query($this->basic_queries['tables'][$i]); $info = $this->db_handler->get_query_info(); } //заполняем таблицу api_access for($i=0; $ibasic_queries['access']); $i++) { $this->db_handler->query($this->basic_queries['access'][$i]); $info = $this->db_handler->get_query_info(); for($j=0; $jbasic_queries['links'][0]; $query = str_replace('!', $info['id'], $query); $query = str_replace('?', $data[$j]['id'], $query); $this->db_handler->query($query); } } } //заполняем таблицу пользователей for($i=0; $ibasic_queries['users']); $i++) { $this->db_handler->query($this->basic_queries['users'][$i]); } return; } }
Класс для авторизации будет настолько простым, насколько это возможно.
interface api_login_itrfc { // mixed autorize(string, string) - в случае успешного // прохождения авторизации, возвращает данные о пользователе // иначе - false public function autorize($username, $password); } class api_login extends api implements api_login_itrfc { protected $api_users = 'api_users'; //таблица пользователей protected $udata = array(); //текущие данные //уже знакомый нам стандартный конструктор public function __construct($db_object) { parent::__construct($db_object); } //имплементируем интерфейс public function autorize($username, $password) { $query = 'select * from `'.$this->api_users. '` where uname = "'.$username.'" and pwd = "'. $this->pwd_hash($password).'"'; $this->udata = $this->db_handler->res_query($query); if(isset($this->udata[0]['uname']) && !empty( $this->udata[0]['uname'])) { return $this->udata[0]; } else { return false; } } // хэш функция вынесена в отдельный protected метод. // вы можете написать тут что угодно. protected function pwd_hash($pwd) { return md5($pwd); } }
Теперь разработаем класс для определения уровня доступа.
interface api_access_itrfc { // bool chk_access(int, string) - получает уровень доступа и функцию, // которую требуется выполнить, возвращает true если уровень доступа // достаточный либо false в противном случае. public function chk_access($access, $f_name); } class api_access extends api implements api_access_itrfc { protected $access = 0; protected $accessible_api = array(); public function __construct($db_object) //стандартный конструктор { parent::__construct($db_object); } public function chk_access($access, $f_name) // проверяем доступ { $array = array(); $flag = true; $array = $this->get_access_data(); if(is_array($array) && !empty($array)) { for($i=0; $i<sizeof($array); $i++) { // ищем функцию к которой необходимо получить доступ // и проверяем текущий уровень доступа и доступ к требуемой // функции путём сравнения установленных бит if(($array[$i]['func_name'] == $f_name) && ($access & $array[$i]['access'])) { $flag = false; break; } } } if($flag) //если флаг не изменил своё значение, // значит с доуступом проблемы { return false; } else { return true; } } //получаем данные из базы. array get_access_data(void) protected function get_access_data() { $query = 'SELECT `'.$this->api_access.'`.access, `'. $this->api_access.'`. role_name, `'.$this->api_table.'`.func_name FROM `'.$this->api_access.'`, `'. $this->api_access_links.'`, `'.$this->api_table.'` WHERE `'.$this->api_access.'`. id = `'.$this->api_access_links.'`.access_id AND `'.$this->api_access_links.'`. api_id = `'.$this->api_table.'`.id '; return $this->db_handler->res_query($query); } }
Напишем простенькие драйверы для двух классов, чтобы проверить, как они работают:
$api_login = null; $api_access = null; $user_data = array(); //$db_handler - объект доступа к базе $api_login = new api_login($db_handler); $api_access = new api_access($db_handler); $user_data = $api_login->autorize('user1', 'pwd1'); echo vd($user_data); echo '<hr>'; if($user_data) { echo vd($api_access->chk_access($user_data['access'], 'get_users')); }
Подведём итоги.
В этой статье мы разработали 2 класса для авторизации и проверки прав доступа, а также класс-утилиту для создания таблиц в базе данных. Авторизация и проверка прав доступа являются самыми первыми и необходимыми действиями для обеспечения безопасности API. Теперь мы можем не просто предоставлять доступ к API нашим клиентам, но предоставлять платный доступ, который может базироваться на уровнях доступа или количестве выполненных запросов к API. Пока клиентский и серверные файл, моделирующие Host 1 и 2 остаются без изменений.
В следующей статье мы разработаем класс API Handler, о котором шла речь в первой статье. API Handler включит в себя выполнение API с вышеприведенными классами. Кроме того, в рамках обсуждения безопасности мы обсудим формат запроса и разработаем дополнительные средства проверки, не связанные с шифрованием.
Код классов находится в аттачменте, ну а я буду рад ответить на ваши дополнительные вопросы в комментариях 😉
В тему:
Доброго времени суток, Кирилл.
Не могли бы Вы в серии своих статей рассказать, как можно организовать работу приложения через iframe на сайте по средствам Вашего API. Так же как это сделано в социальных сетях: ВК, FB и других.
Gena, April 1, 2011 8:10 pmдоброе время суток. тут, собственно, не о чем рассказывать.
iframe позволяет загружать в себя любой HTML-код с помощью параметра src=URL
например, вконтакте предоставляет JS API с помощью вызова функции
vk.init
(
function()
{
//….
}
);
чтобы этот объект стал доступен, достаточно просто подключить JS файл с правильным параметром src=URL в теге script – понятно, что этот файл хранится на сервере вконтакте, но JS – это клиентский язык, т.о. этот файл вполне можно подключить у себя.
точно так же и iframe, расположенный на стороне вконтакте, способен загрузить по указанному src=URL стороннее приложение.
например, есть тег object через который можно подключить Java или Flash программу. понятно, что эти программы могут использовать любые сторонние ресурсы по TCP/IP для своих нужд.
т.о. по сути, чтобы подключить своё приложение к вконтакте, нужно
1. выполнить все требования вконтакте к приложению
2. обеспечить вконтакте URLом своего приложения, который они и впишут в свой iframe
в этом случае, клиентское приложение будет точно так же использовать API функции (в принципиальном смысле), как я показал в своих статьях.
принцип, по которому iframe или обычный frame выводит HTML в браузер, абсолютно аналогичен обычному выводу в браузер. единственный нюанс – iframe и frame отсылают браузеру пользователя дополнительные заголовки (т.к. содержут на 100% завершённую HTML страницу). лично я из-за этого свойства их крайне нелюблю.
в своих статьях я показал пример клиентского приложения (класс connector_dogbody), которое пользуется удалённым API. параметр src=URL тега iframe вполне может содержать URL файла, который создаёт и использует объект connector_dogbody
другими словами, пример моего API – это взаимодействие двух PHP программ на разных серверах. вывод в клиентский браузер зависит целиком от бизнес-логики и алгоритм работы PHP приложений никак не изменится от того, куда будет отправлен результирующий HTML – в обычный браузер или в элемент iframe.
возможно, я не так понял ваш вопрос и вы бы хотели увидеть пример JS реализации API. к сожалению, этого я показать не смогу, т.к. не особо силён в JS. но суть всё та же (на примере вконтакте) –
1. подключаем удалённый JS файл
2. создаём объект и инициализируем его.
3. вызваем через этот объект JS API методы.
Большое спасибо за ответ! Попробую разобраться с js 🙂
Gena, April 1, 2011 10:11 pm