Часть 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.
Пример сервиса, написанного с использованием библиотеки 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. Пользовательские типы данных тоже должны быть строго специфицированые.
Типы данных описываются в отдельной секции wsdl-документа.
Клиент из .Net может взаимодействовать с SOAP-сервисом через супер-мощную встроенную библиотеку WCF. Для этого в VS-проекте нужно лишь добавить ссылку на сервис (Add service reference):
//Класс 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-файл:
Как это выглядит на сервере.
От клиента серверу в заголовке сообщения приходит что-то вроде:
Заголовок легко парсится в нужные переменные, и код авторизации менять, по сути, не надо. Если вы используете NuSOAP, то заголовок придется парсить вручную: не умеет NuSOAP разбирать SOAP:Header.
И напоследок. Чтобы хоть как-то облегчить свою участь, можно тестить сервисы с помощью Codeception.
<endpoint address="http://.../service.php" >
<headers>
<securitytoken>abcdef12345%$#</securitytoken>
</headers>
</endpoint>
Как это выглядит на сервере.
От клиента серверу в заголовке сообщения приходит что-то вроде:
<s:Header>
<securitytoken>1a232d4dg35</securitytoken>
</s:Header>
Заголовок легко парсится в нужные переменные, и код авторизации менять, по сути, не надо. Если вы используете NuSOAP, то заголовок придется парсить вручную: не умеет NuSOAP разбирать SOAP:Header.
И напоследок. Чтобы хоть как-то облегчить свою участь, можно тестить сервисы с помощью Codeception.
0 коммент.:
Отправить комментарий