love via soap II: php + c#

27 февр. 2013 г. | | |

Часть 1, в которой рассказывалось, зачем это всё нужно, и как несложно реализуется взаимодействие  php-клиента с asmx-сервисом.
В этой части мы будем писать SOAP-сервис на php, а клиентскую часть - на C#.Так что пристегнись, это php!
Для построения SOAP-сервера в php есть класс SoapServer, который поддерживает протоколы SOAP 1.1 и SOAP 1.2. Что он не поддерживает - так это автоматическую генерацию WSDL-документа для сервиса, который необходим для коммуникации между клиентом и сервером. Помните, как VS заботливо генерировала его для нас? Но это php. Тут есть несколько путей:
1. Создавать wsdl ручками
2. Использовать сторонние библиотеки (NuSOAP, PEAR::SOAP, WSHelper) или встроенные в используемый фреймворк хелперы (например, Zend_Soap_Server из Zend Framework), сильно облегчающие жизнь.
Понятно, что первый путь - для совсем уж крепких духом ребят. Во втором случае рутинная работа будет сделана (и генерация wsdl-документа) за нас. Останется только добавить классы, представляющие сервисы, и всё. Очень советую идти этим путём: сэкономите много сил и времени.
Пример сервиса, написанного с использованием библиотеки WSHelper.
/* service.php */
require_once "config.php";
require_once "common.php";
if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) && 
/* тут код авторизации */)
{
    require_once "classes/compress.class.php";
    compress_start();
    $sesrvice_name = $_GET['class'];
    // старт SOAP-сервера
    $WSHelper = new WSHelper(WSURI, $cache_dir, $service_name);
    //Обработка запроса
    $WSHelper->handle();
    compress_out();  
}
else
    {
     
    header("WWW-Authenticate: Basic realm=\"Webservice\"");
     
    header("HTTP/1.0 401 Unauthorized");
         die("No access!");
   
/* updateservice.php */
class updateservice {
     /**
      * Gets the latest version for specified version
      * @param string[]
      * @return release[]
     */
      public function GetLastVersionForModule($modules)
      {
      $updates = array();
      $query = "SELECT * FROM `releases` WHERE `IsPublic`=1 AND `ModuleID` IN (";
      foreach($modules as $key=>$moduleID)
      {
        $query.= "'" . htmlspecialchars($moduleID) . "', ";
      }
      $query = substr($query, 0, strlen($query)-2) .")";
      $result = DB::GetConnection()->query($query);
      if ($result != FALSE AND $result->num_rows>0)
      {
        while ($row = $result->fetch_object()) {
            $updates[] = $row;
        }
      }
      return $updates;
    }
}
Что тут происходит? Клиент отправляет запрос по адресу http://somehost/service.php?class=updateservice&wsdl. Get-параметр class - это как раз имя класса, который будет обрабатывать запрос.  В скрипте service.php после проверки авторизации запускается обёртка WSHelper поверх стандартного SOAP-сервера и обрабатывается запрос.  Для того, чтобы был сгенерирован правильный wsdl-документ, метод класса, представляющего сервис, должен иметь определённые типы входных параметров (@param) и возвращаемого значения (@return). Спецификации делаются в виде phpdoc-комментариев перед именем метода. В примере на вход сервису нужен массив строк, а возращается массив объектов типа release. Пользовательские типы данных тоже должны быть строго специфицированые.
//Класс release
class release {
    /** @var string */
    public $ModuleID;   
    /** @var string */
    public $Version;
    /** @var string */
    public $Date;
    /** @var string */
    public $Description;       
}

Типы данных описываются в отдельной секции  wsdl-документа.

Клиент из .Net может взаимодействовать с SOAP-сервисом через супер-мощную встроенную библиотеку WCF. Для этого в VS-проекте нужно лишь добавить ссылку на сервис (Add service reference):
Важно нажать на кнопку Advanced в левом нижнем углу окошка "Add Service Reference" и поставить галочку "Allow generation of asynchronous operations" в появившемся окне. Этим мы скажем Visual Studio сгенерировать асинхронные вызовы методов веб-сервиса.
Теперь можно обращаться к сервису из десктоп-приложения или из другого сервиса на C#. В примере ниже клиент проверяет наличие новых версий програмных модулей на сервере через async-запрос к Soap-сервису.

var updateserv = new updateservicePortTypeClient();

//настройки аутентификации для работы с сервисом
updateserv.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
updateserv.ClientCredentials.Windows.AllowNtlm = false;
updateserv.ClientCredentials.UserName.UserName = settings.Email;
updateserv.ClientCredentials.UserName.Password = settings.UserID;


//подписываемся на обработку события по окончанию получения данных от сервиса updateserv.GetLastVersionForModuleCompleted += updateserv_GetLastVersionForModuleCompleted;


//открываем соединение
updateserv.Open();


//вызываем метод веб-сервиса

updateserv.GetLastVersionForModuleAsync(modules);
/*....*/


void updateserv_GetLastVersionForModuleCompleted(object sender, GetLastVersionForModuleCompletedEventArgs e)
{
//если всё прошло без ошибок, супер!
   if (e.Error == null)
   {
      //делаем что-то с полученными данными
   };
//обработка ошибок
}

Пара слов об авторизации

Очень удобно было использовать в сервисах  basic HTTP аутентификацию PHP:

if (!isset($_SERVER['PHP_AUTH_USER']) && !isset($_SERVER['PHP_AUTH_PW']))
{
  Header("WWW-Authenticate: Basic realm=\"My realm\"");
  Header("HTTP/1.0 401 Unauthorized"); 
  exit;
}


Но оказалось, что в Apache, когда php стоит как FastCGI, такой подход не работает: не установлены переменные PHP_AUTH_USER и PHP_AUTH_PW. Есть хак, решающий эту проблему.
В .httaccess добавить
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]
</IfModule>
Перед кодом авторизации вставить следующий код:

list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6))); 
 
Всё, после этого базовая авторизация должна снова работать.
Если никак не помогает, можно с клиента посылать Soap-серверу аутентификационные данные в заголовке Soap-сообщения. Распарсить заголовок на сервере - это уже дело техники. 
Как это выглядит на клиенте.
Добавляем дополнительный заголовок к вызову WCF-сервиса:
var updateserv = new updateservicePortTypeClient();
var channel = (IContextChannel)updateserv.InnerChannel;
using(OperationContextScope scope = new OperationContextScope(channel))
{
     var securitytoken = Convert.ToBase64String(Encoding.UTF8.GetBytes(_token));
     OperationContext.Current.OutgoingMessageHeaders.Add(
         MessageHeader.CreateHeader("securitytoken", "", securitytoken)
     );
     updateserv.GetLastVersionForModuleAsync(modules);
}
Нужно учесть, что заголовок с аутентификационными данными нужно добавлять для всех вызовов Soap-сервисов. Чтобы не писать дополнительный код (если заголовок одинаковый во всех случаях), можно легко добавить custom header для WCF-вызовов через config-файл:

<endpoint address="http://.../service.php" >
    <headers>
        <securitytoken>abcdef12345%$#</securitytoken>
    </headers>
</endpoint>

 
Как это выглядит на сервере.
От клиента серверу в заголовке сообщения приходит что-то вроде:

<s:Header>
   <securitytoken>1a232d4dg35</securitytoken>
</s:Header>



Заголовок легко парсится в нужные переменные, и код авторизации менять, по сути, не надо.  Если вы используете NuSOAP, то заголовок придется парсить вручную: не умеет NuSOAP разбирать SOAP:Header.
И напоследок. Чтобы хоть как-то облегчить свою участь, можно тестить сервисы с помощью Codeception.

0 коммент.:

Отправить комментарий