Создаём собственный API-5

Кирилл Евсеев, March 31, 2011

Ну что ж, пришло время поговорить о безопасности. Но прежде чем мы приступим, я немного вернусь назад. В самой первой статье я говорил о том, что информацию можно вернуть вызываемому приложению как объект. Как вернуть информацию в виде массива мы уже разобрались. Массив – это просто данные, которыми удобно манипулировать.

Если вы хотите предоставить удалённым разработчикам объект, который, в т.ч., может обращаться к Host 1 для вызова API, достаточно просто создать этот объект на стороне API-сервера и вернуть вызываемому приложению вместе с классом (и всеми родительскими классами), который является типом этого объекта. Сделать это очень просто. Достаточно прочитать файл, в котором хранится определение класса, с помощью стандартных функций типа fopen и вернуть в качестве строки. На стороне приложения (Host 2) эту строку нужно сохранить в локальный файл (фактически получим копию файла на API-сервере) и подключить этот файл с помощью incude.

О том, как передаются сериализованные объекты, можно подробнее прочитать в справке PHP.
Теперь вплотную займёмся безопасностью. Условно, меры, которые мы можем предпринять, можно разделить на следующие пункты:

  1. Авторизация с уровнями доступа.
  2. Фильтрация запроса.
  3. Использование защищённого соединения.
  4. Использование различных трюков.

В этой части статьи мы обсудим авторизацию и подготовимся к п.2. Что касается защищённого соединения – речь идёт о передаче данных по протоколу HTTPS, с которым cURL отлично работает.

Однако, можно продолжать передавать данные по HTTP, но добавить к нашему протоколу функцию шифрования. Это просто бездонная тема и останавливаться на этом мы не будем (возможно, мы рассмотрим некоторые идеи в других статьях, не касающихся API). В любом случае, относительно п.4., – я подкину вам пару идей, которые вы можете использовать в своих рабочих приложениях уже сейчас:

  1. В запросе передавать не имена функций, а их хэши.
  2. Клиент шифрует свой запрос функцией encrypt_foo_client и передаёт его API-серверу. API-сервер шифрует запрос функцией encrypt_foo_server и возвращает его API-клиенту. API-клиент расшифровывает запрос функйцией decrypt_foo_client и возвращает запрос серверу. Сервер расшифровывает запрос функцией decrypt_foo_server и приступает к работе. (см. подробнее симметричные/ассимитричные криптосистемы, криптосистемы с открытыми/закрытыми ключами)
  3. Использовать контрольную сумму, хэш случайной строки и т.п., которые будут каждый раз проверятся, чтобы установить подлинность запроса.

На самом деле, если речь идёт о деньгах, все эти танцы с бубном вполне оправданы. Но, как учит нас мистер Кевин Митник – какой бы сложной ни была система, критическая уязвимость – это человеческий фактор :) Так что приступим к авторизации и оставим проблемы безопасности данных на совесть вашего бюджета.
Итак, нам понадобится ещё одна утилита для создания дополнительных таблиц в базе.

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 с вышеприведенными классами. Кроме того, в рамках обсуждения безопасности мы обсудим формат запроса и разработаем дополнительные средства проверки, не связанные с шифрованием.

Код классов находится в аттачменте, ну а я буду рад ответить на ваши дополнительные вопросы в комментариях ;)

В тему:

3комментария

Доброго времени суток, Кирилл.

Не могли бы Вы в серии своих статей рассказать, как можно организовать работу приложения через iframe на сайте по средствам Вашего API. Так же как это сделано в социальных сетях: ВК, FB и других.

Gena, April 1, 2011 8:10 pm Reply

доброе время суток. тут, собственно, не о чем рассказывать.
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 методы.

Кирилл Евсеев, April 1, 2011 9:03 pm Reply

Большое спасибо за ответ! Попробую разобраться с js :)

Gena, April 1, 2011 10:11 pm Reply
Ваше имя
Ваш email*
Ваш сайт
Текст вашего комментария:

Поиск по блогу:
Подписаться:
Популярные:
Облако тегов:
Разное:
Счетчик: