XML-шлюз своими руками

    Если вам приходилось работать с платежными системами, сервисами регистрации и другими ресурсами, реализующими удаленное взаимодействие с центральным сервером, то наверняка сталкивались с понятием «шлюз». Это сервис, который предназначен для проведения операций на центральном сервере путем обмена электронными сообщениями между центральным сервером (сервером системы, имеющей шлюз) и программным обеспечением некого клиента.

    Пожалуй, идеальный шлюз должен уметь принимать и отдавать данные в формате XML, хотя бы потому, что это стандартизированный, самодокументируемый формат, поддержка которого реализована во всех современных языках программирования (и даже на аппаратном уровне). Кроме того, XML поддерживает Юникод-кодировки UTF-8, UTF-16 и даже UTF-32. Вкратце, но с примерами, расскажу о принципах создания простых XML-шлюзов. Отправку запросов, простоты и, одновременно, разнообразия ради рассмотрим на примере POST/GET методов.

    Для начала рассмотрим концепцию взаимодействия. Вы – это сервер, который будет принимать запросы, обрабатывать их и отдавать клиенту запрошенную информацию в типизированным формате.

    image

    Предлагаю для наглядности рассмотреть следующий пример: у вас на сервере есть некая база информационных материалов, которые вы готовы предоставлять за абонентскую плату. Материал разбит на категории, существует база данных партнеров в которой содержится информация о том, к каким категориям каждый партнер имеет доступ.

    Для того, что бы партнер мог получать материал только в пределах своего доступа нам необходимо четко идентифицировать его. В противном случае он или любой другой смекалистый сможет «вытянуть» из вас больше положенного, а это чревато подмоченной репутацией и финансовыми потерями, т.к. проект у нас коммерческий.

    Решить проблему идентификации можно за счет выдачи партнерам уникальных ключей доступа, которые будут сгенерированы с привязкой к IP-адресу сервера, осуществляющего запрос к шлюзу.

    Например, есть компания ОАО «Шишкин лес», которая оплатила пожизненный доступ к материалам из категорий 1, 2, 5 и 9. Сервер компании стоит дома под кроватью директора, но IP адрес статический и мы его знаем – 12.34.56.78. Теперь нам нужно сгенерировать уникальный ключ доступа для этого партнера, что можно сделать примерно так:
    1. <?php
    2.  
    3. define('SECRET_WORD', 'LsXUS~J');
    4.  
    5. function getUniqueKey($ip, $access) {
    6.  
    7.  return substr(md5($ip . SECRET_WORD . $access), 5, 20);
    8.  
    9. }
    10.  
    11. ?>

    Обратите внимание на константу «SECRET_WORD», которая является постоянной вне зависимости от прочих условий. Функция getUniqueKey() принимает в качестве параметров IP-адрес сервера ($ip = ’12.34.56.78’) и строку сложенных ID-категорий, к которым у партнера есть доступ ($access = ‘1259’). Далее генерируется md5-хэш из всех этих параметров, который обрезается до 15-символьной строки (начиная, в нашем примере, с 5-го символа хэша). Это и есть наш уникальный ключ, который генерируется для каждого партнера. Корректность ключа мы будем проверять при каждом запросе от клиента.

    Как обсуждалось ранее, запрос к серверу клиент может осуществить посредством POST/GET методов, передав в них необходимые параметры. Рассмотрим пример такого запроса:
    mysite.com/gateway.php?id=1&type=list&token=8381ad87b37986ac7bb6

    Итак, мы осуществляем запрос к шлюзу и передаем ему всего 3 параметра:
    • id=1, персональный ID клиента в вашей системе
    • type=list, это придуманный для мною параметр, который сообщает серверу что именно и в каком виде клиент хочет от него получить
    • token=8381ad87b37986ac7bb6, уникальный ключ клиента, который он отправляет серверу при каждой транзакции для подтверждения подлинности

    Шлюзу (gateway.php) остается только проверить подлинность подключения и ответить клиенту на запрос. Ответить, кстати, нужно в любом случае, даже если ключ неправильный или возникла ошибка выборки/формирования ответа – клиент все равно обязан получить ответ.

    Рассмотрим функцию проверки ключа при запросе к шлюзу:
    1. <?php
    2.  
    3. function checkValidation($clientID, $clientToken) {
    4.  
    5.  // IP-адрес клиента
    6.  $clientRemoteAddr = (getenv('HTTP_X_FORWARDED_FOR')) ? getenv('HTTP_X_FORWARDED_FOR') : getenv('REMOTE_ADDR');
    7.  
    8.  // Уровень доступа клиента к категориям
    9.  // На самом деле здесь должна быть выборка
    10.  // прав доступа клиента по его $clientID
    11.  $clientAccessLevel = '1259';
    12.  
    13.  // Правильный ключ
    14.  $checkToken = getUniqueKey($clientRemoteAddr, $clientAccessLevel);
    15.  
    16.  // Сравнение проверочного и полученного от клиента ключей
    17.  return ($clientToken == $checkToken);
    18.  
    19. }
    20.  
    21. ?>

    Таким образом, если функция checkValidation() возвращает истину, то проверка подлинности подключения пройдена. Теперь можно приступать к формированию и подготовке ответа.

    В нашем примере ответ должен содержать некую выборку информации из разных категорий, предположим, что такая выборка содержит следующий набор полей:
    • element_id – идентификатор каждой конкретной записи, уникален
    • category_id – идентификатор рубрики, к которой относится запись (напомню, что у в нашем примере клиент имеет доступ к рубрикам 1, 2, 5 и 9)
    • title – заголовок элемента
    • text – содержимое записи
    • time – дата и время публикации записи (формат даты выбирайте сами)

    Структура ответа в таком случае будет примерно такой:
    1. <?xml version="1.0" encoding="utf-8"?>
    2. <response>
    3.  <error-code>0</error-code>
    4.  <content>
    5.   <item>
    6.    <element_id>1</element_id>
    7.    <category_id>5</category_id>
    8.    <title>Заголовок 1</title>
    9.    <text>Текст элемента номер 1, про всякую фигню</text>
    10.    <time>1248770309</time>
    11.   </item>
    12.   <item>
    13.    <element_id>2</element_id>
    14.    <category_id>9</category_id>
    15.    <title>Заголовок 2</title>
    16.    <text>Текст элемента номер 2, про всякую фигню</text>
    17.    <time>1248770319</time>
    18.   </item>
    19.  </content>
    20. </response>

    Атрибут «error-code» в случае успешного осуществления операции должен быть равнее нулю, что буквально говорит клиенту «все хорошо, ниже что ты просил». Если возникла какая-либо ошибка, то в данном атибуте передается код этой ошибки (атрибута «content» и всего его содержимого в данном случае уже не будет).

    Предположим, что наш обработчик ошибок знает следующие возможные проблемы:
    • Ошибка 100 – ошибка проверки ключа (token не соответствует действительности)
    • Ошибка 101 – не передан ID клиента
    • Ошибка 102 – не передан уникальный ключ клиента
    • Ошибка 201 – запрошенная информация не найдена в базе данных
    • Ошибка 202 – возникла ошибка при выборе запрошенной информации


    Ну и так далее. Ответ с ошибкой будет таким:
    1. <?xml version="1.0" encoding="utf-8"?>
    2. <response>
    3.  <error-code>102</error-code>
    4. </response>

    Последнее, что осталось рассмотреть – это формирование такого ответа. Реализаций, как и всего вышеописанного, может быть много, но я предложу такой простой вариант:
    1. <?php
    2.  
    3. function sendResponse($code, $content = '') {
    4.  
    5.  // Создание атрибута
    6.  function getNode($key, $value) {
    7.   if ($value != '') {
    8.    return '<' . $key . '>' . $value . '</' . $key . '>';
    9.   }
    10.  }
    11.  
    12.  // Дополнительные поля
    13.  $items = '';
    14.  
    15.  // Если $content не пуста и является массивом
    16.  if (!empty($content) && is_array($content)) {
    17.   foreach ($content as $key => $value) {
    18.    $items .= '<user>';
    19.    if (is_array($value)) {
    20.     foreach ($value as $skey => $svalue) {
    21.      $items .= getNode($skey, $svalue);
    22.     }
    23.    } else {
    24.     $items .= getNode($key, $value);
    25.    }
    26.    $items .= '</user>';
    27.   }
    28.  }
    29.  
    30.  // Формирование и отправка ответа
    31.  print '<response><error-code>' . $code . '</error-code>' . $items . '</response>';
    32.  exit();
    33.  
    34. }
    35.  
    36. ?>

    Надеюсь, эта небольшая статья была интересна. Предложенная реализация, за некоторыми доработками, вполне подойдет для использования на небольших ресурсах. При большом трафике результаты выдачи необходимо кэшировать, но это, надеюсь, все итак знают.

    Критика, дополнения, другие реализации в комментариях приветствуются :-)
    Поделиться публикацией

    Комментарии 26

      +3
      Два момента:
      — название статьи почти не связано с содержанием
      — вместо кастомных кодов ответов лучше пользоваться стандартными HTTP: и понятны, и расшифровка не требуется.

      Также непонятно, что вы сделали полезного :) Но это совсем субъективно.
        –1
        В материале рассказал о том, как организовать контролируемую отдачу данных тем или иным пользователям (группам пользователей). Одно дело общедоступный информер аля-погода, другое дело передача конфиденциальной информации.

        HTTP'ых ответов, к сожалению, не хватит для описания всех случаев. Создание таблицы кастомных кодов — нормальная практика. Платежные системы, например, так и делают.

        Полезность — вещь действительно субъективная :) Возможно имело смысл посмотреть на проблему с более глубокой точки зрения — создание защищенных соединений, политика кэширования… Но я решил начать с простого, посмотреть интересно ли это людям иль нет.
          0
          Может стоит отдавать HTTP ответы при ошибке авторизации?
            0
            Вполне, почему бы и нет. Но если вы захотите уточнить ошибку (неправильный ключ, битый токен, нехватка параметров для идентификации), то ошибку придется закастомайзить.
              0
              У iBear все правильно. В данном примере, http это транспортный протокол. А xml — протокол приложения, для маршрутизации которого, собственно и создается шлюз. Их не нужно смешивать.
                0
                Тоже соглашусь с тем, что не стоит мешать http ошибки и ошибки шлюза. Сам писал несколько шлюзов, а так же пожключался к многим другим. Как правило http ошибка говорит о том, что сообщение не удалось обработать/доставить и т.д. Ошибка же в ответе говорит именно про ошибку по протоколу.
                В моей практике было несколько случаев когда в шлюзе комбинировали http ошибки и ошибки в xml ответе (были специфичные ошибки). Это все приводило к тому, что приходилось писать две обработки ответа, одна по http другая уже после успешного получения xml — в итоге немного разбросанный код.
                Еще проблема в том, если в будущем возникнет необходимость использовать не только http для транспорта.
              0
              Да не, вы меня не поняли :)

              Я, во-первых, имел в виду, что на XML все не завязано — это может быть любой ресурс.

              Во-вторых, конкретно в вашем примере с реестром кастомных кодов ответов можно и не заморачиваться: в приведенном случае хватит стандартных HTTP-response; просто раз уж решили упрощать до упора все, даже выдачу токена, не увидел смысла вводить коды. Но вам виднее, конечно.

              Насчет полезности — к тому, что вместить блок с xml на 20 строк и еще на 10 расшифровку кодов не поленились, а нормальную систему токенов/авторизации оставили за бортом. Я думаю, что это был важный вопрос; если у вас есть наработки в этой области, поделитесь :)
                0
                ОК, замечание принято :)

                Могу попозже написать продолжение, в котором сделаю акцент именно на безопасность транзакций (шифрование данных, создание подписей, WS-Security, SAML, интеграцию LDAP или Active Directory, XKMS, SSL конечно же… HTTP-авторизацию тоже можно не забывать), рассказать о более сложной схеме:
                Клиент -> SSL/TLS -> Контент-свитчер -> Шлюзы -> Сервера/сервисы
            +1
            Насчет привязки токена авторизации к доступным категориям, это конечно хорошо, но получается что клиенту каждый раз придется отдавать не только токен но и список всех категорий, даже если необходимо получить записи из всех категорий. Не проще ли дать юзеру логин с паролем и по нему авторизовать и отдавать временный токен.
              0
              *получить запись из одной категории*
                –1
                Конечно же привязывать к категориям не обязательно, я привел это в качестве примера, как второй возможный параметр. Им, например, может быть ID какого-то мероприятия, в рамках которого действует партнер. Либо такого параметра может не быть вовсе.

                Логин/пароль — тоже вполне себе хорошая схема. Можно хоть проверять правильность связки, хоть HTTP Basic access authentication вешать на доступ к шлюу. Но мне привязка к IP сервера больше импонирует, т.к. пароль можно «потерять».
                  0
                  Не проще переписать на DOMXML функционале?
                0
                Генерация мне не нравится, поэтому внесу своих 5 копеек :)

                <?php
                $s = simplexml_load_string('<?xml version="1.0" encoding="utf-8"?><response></response>');
                $s->{'error-code'} = 0;
                $content = $s->addChild('content');

                //эмуляция данных
                $i = array(
                array('element_id'=>1,
                'category_id'=>5,
                'title'=>'Заголовок 1',
                'text'=>'Текст элемента номер 1, про всякую фигню',
                'time'=>'1248770309'
                ),
                array('element_id'=>2,
                'category_id'=>9,
                'title'=>'Заголовок 2',
                'text'=>'Текст элемента номер 2, про всякую фигню',
                'time'=>'1248770319'
                )
                );

                //генерация
                foreach ($i as $v) {
                $item = $content->addChild('item');
                $item->element_id = $v['element_id'];
                $item->category_id = $v['category_id'];
                $item->title = $v['title'];
                $item->text = $v['text'];
                $item->time = $v['time'];
                }

                echo $s->asXML();

                dumpz.org/10997/
                  0
                  Реализацию я оставляю на совесть исполнителя, о чем писал выше ;)

                  Еще можно использовать XMLWriter или DOMDocument — как душе поэта угодно!
                  +4
                  Зря вы HTTP_X_FORWARDED_FOR используете.
                  В этот хидер можно прописать все что угодно, таким образом подделав IP.

                  Тоесть человек может зайти с любого IP прописав требуемый IP в этот хидер.
                    0
                    Точно. HTTP_X_FORWARDED_FOR — для рюшечек всяких, не для контроля безопасности. В смысле IP единственная информация, которой можно доверять — REMOTE_ADDR. Если его нет — значит нет информации об IP, достойной доверия.
                      0
                      Кстати, очень ценное замечание. Действительно HTTP_X_FORWARDED_FOR и HTTP_CLIENT_IP можно подменить.
                        0
                        вообще HTTP_* можно подменить, если я не ошибаюсь. Так что на них надежды нет.
                      +1
                      немного похоже на схему аутентификации вконтакте.
                      Второй момент, сложение категорий, у Вас получилось «1259», а если категорий больше 10?
                        0
                        ну это, конечно, мелочи
                          0
                          Сложение категорий совсем не обязательно, это был просто пример второго юзер-зависимого атрибута. Его можно исключить полностью, либо заменить на что-то другое.
                            0
                            да я понял, что пример, вот и подписал ниже, что это мелочи :)
                              0
                              Вы в одном параметре token смешали access и authentication. Практики этот подвох чувствуют нутром. В такой схеме, появляются ненужные аномалии. Например, клиент проплативший доступ еще к одной категории документов, автоматически теряет возможность подключаться к ресурсу, пока не обновит ключ на своем сервере.
                            –1
                            С открытием вас Америки, капитан!
                              0
                              А не проще ли использовать вместо этого SOAP-веб-сервис?
                              Реализация сервера и клиентов здорово упрощается за счет использования готовых компонентов для работы с SOAP. В частности, в PHP есть расширение soap.so и Pear Soap.
                                0
                                А не проще ли вместо мыла использовать XML-RPC? Странно, что про него еще никто не сказал.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое