Подключение к защищенной веб-службе WS-Security с помощью PHP
Я пытаюсь подключиться к веб-службе, которая защищена паролем, а URL-адрес - https. Я не могу понять, как выполнить аутентификацию, прежде чем script сделает запрос. Похоже, он делает запрос, как только я определяю услугу. Например, если я добавлю:
$client = new SoapClient("https://example.com/WSDL/nameofservice",
array('trace' => 1,)
);
а затем перейдите на сайт в браузере, я получаю:
Fatal error: Uncaught SoapFault exception:
[WSDL] SOAP-ERROR: Parsing WSDL: Couldn't load from
'https://example.com/WSDL/nameofservice' in /path/to/my/script/myscript.php:2
Stack trace: #0 /path/to/my/script/myscript.php(2):
SoapClient->SoapClient('https://example...', Array) #1 {main} thrown in
/path/to/my/script/myscript.php on line 2
Если я попытаюсь определить службу как Soap Server, например:
$server= new SoapServer("https://example.com/WSDL/nameofservice");
Я получаю:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>WSDL</faultcode>
<faultstring>
SOAP-ERROR: Parsing WSDL:
Couldn't load from 'https://example.com/WSDL/nameofservice'
</faultstring>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Я еще не пытался отправить необработанный конверт запроса, чтобы увидеть, что возвращает сервер, но это может быть обходным путем. Но я надеялся, что кто-то скажет мне, как я могу настроить его, используя встроенные классы php. Я попытался добавить в массив "userName" и "password", но это было бесполезно. Проблема в том, что я даже не могу сказать, попадаю ли я на удаленный сайт вообще, не говоря уже о том, отказывается ли он от запроса.
Ответы
Ответ 1
Проблема заключается в том, что документ WSDL каким-то образом защищен (базовая аутентификация - я не думаю, что аутентификация дайджеста поддерживается с помощью SoapClient
, поэтому в этом случае вам не повезло) и что SoapClient
поэтому не может прочитать и разобрать описание сервиса.
Прежде всего, вы должны попытаться открыть местоположение WSDL в своем браузере, чтобы проверить, представлено ли вам диалоговое окно проверки подлинности. Если есть диалог проверки подлинности, вы должны убедиться, что SoapClient
использует требуемые учетные данные для входа в документ WSDL. Проблема заключается в том, что SoapClient
будет отправлять учетные данные, предоставленные с параметрами login
и password
(а также параметр local_cert
при использовании аутентификации сертификатов) при создании клиента при вызове службы, а не при извлечении WSDL (см. здесь). Для решения этой проблемы существует два метода:
-
Добавьте учетные данные для входа в URL-адрес WSDL при вызове конструктора SoapClient
$client = new SoapClient(
'https://' . urlencode($login) . ':' . urlencode($password) . '@example.com/WSDL/nameofservice',
array(
'login' => $login,
'password' => $password
)
);
Это должно быть самое простое решение, но в PHP Bug # 27777 написано, что это тоже не сработает (я не знаю, t попробовал это).
-
Извлеките WSDL вручную с помощью обтекателя потока HTTP или ext/curl
или вручную через браузер или через wget
, например, сохраните его на диске и создайте экземпляр SoapClient
со ссылкой на локальный WSDL.
Это решение может быть проблематичным, если документ WSDL изменяется, поскольку вам необходимо обнаружить изменение и сохранить новую версию на диске.
Если диалоговое окно проверки подлинности не отображается, и если вы можете прочитать WSDL в своем браузере, вы должны предоставить дополнительную информацию, чтобы проверить другие возможные ошибки/проблемы.
Эта проблема окончательно не связана с самой службой, поскольку SoapClient
задерживается уже при чтении документа описания службы перед выдачей вызова самой службе.
EDIT:
Наличие локального WSDL файла - это первый шаг - это позволит SoapClient
знать, как взаимодействовать с сервисом. Неважно, если WSDL напрямую обслуживается из местоположения службы, с другого сервера или считывается из локального файла - служебные URL кодируются в WSDL, поэтому SoapClient
всегда знает, где искать конечную точку службы.
Вторая проблема заключается в том, что SoapClient
не поддерживает спецификации WS-Security, что означает, что вы должны расширить SoapClient
для обработки конкретных заголовков. Точкой расширения для добавления требуемого поведения будет SoapClient::__doRequest()
, который предварительно обрабатывает полезную нагрузку XML перед отправкой в конечную точку службы. Но я думаю, что для реализации самого решения WS-Security потребуется достойное знание конкретных спецификаций WS-Security. Возможно, заголовки WS-Security также могут быть созданы и упакованы в XML-запрос с помощью SoapClient::__setSoapHeaders()
и соответствующего SoapHeader
, но я сомневаюсь, что это сработает, оставив пользовательское расширение SoapClient
как одинокую возможность.
Простым расширением SoapClient
было бы
class My_SoapClient extends SoapClient
{
protected function __doRequest($request, $location, $action, $version)
{
/*
* $request is a XML string representation of the SOAP request
* that can e.g. be loaded into a DomDocument to make it modifiable.
*/
$domRequest = new DOMDocument();
$domRequest->loadXML($request);
// modify XML using the DOM API, e.g. get the <s:Header>-tag
// and add your custom headers
$xp = new DOMXPath($domRequest);
$xp->registerNamespace('s', 'http://www.w3.org/2003/05/soap-envelope');
// fails if no <s:Header> is found - error checking needed
$header = $xp->query('/s:Envelope/s:Header')->item(0);
// now add your custom header
$usernameToken = $domRequest->createElementNS('http://schemas.xmlsoap.org/ws/2002/07/secext', 'wsse:UsernameToken');
$username = $domRequest->createElementNS('http://schemas.xmlsoap.org/ws/2002/07/secext', 'wsse:Username', 'userid');
$password = $domRequest->createElementNS('http://schemas.xmlsoap.org/ws/2002/07/secext', 'wsse:Password', 'password');
$usernameToken->appendChild($username);
$usernameToken->appendChild($password);
$header->appendChild($usernameToken);
$request = $domRequest->saveXML();
return parent::__doRequest($request, $location, $action, $version);
}
}
Для базовой проверки подлинности WS-Security вам нужно добавить в SOAP-заголовок следующее:
<wsse:UsernameToken>
<wsse:Username>userid</wsse:Username>
<wsse:Password>password</wsse:Password>
</wsse:UsernameToken>
Но, как я сказал выше, я думаю, что для получения этой работы требуется гораздо больше знаний о спецификации WS-Security и данной архитектуры сервиса.
Если вам требуется решение корпоративного класса для всего диапазона спецификации WS- *, и если вы можете установить модули PHP, вы должны посмотреть на WSO2 Web Services Framework для PHP (WSO2 WSF/PHP)
Ответ 2
Просто добавьте SoapHeader для создания аутентификации компилятора Wsse:
class WsseAuthHeader extends SoapHeader {
private $wss_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd';
function __construct($user, $pass, $ns = null) {
if ($ns) {
$this->wss_ns = $ns;
}
$auth = new stdClass();
$auth->Username = new SoapVar($user, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Password = new SoapVar($pass, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$username_token = new stdClass();
$username_token->UsernameToken = new SoapVar($auth, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns);
$security_sv = new SoapVar(
new SoapVar($username_token, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns),
SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'Security', $this->wss_ns);
parent::__construct($this->wss_ns, 'Security', $security_sv, true);
}
}
$wsse_header = new WsseAuthHeader($username, $password);
$x = new SoapClient('{...}', array("trace" => 1, "exception" => 0));
$x->__setSoapHeaders(array($wsse_header));
Если вам нужно использовать ws-security с nonce и меткой времени, Peter опубликовал версию обновления http://php.net/manual/en/soapclient.soapclient.php#114976, из которой он написал, что это сработало для него:
class WsseAuthHeader extends SoapHeader
{
private $wss_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd';
private $wsu_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd';
function __construct($user, $pass)
{
$created = gmdate('Y-m-d\TH:i:s\Z');
$nonce = mt_rand();
$passdigest = base64_encode(pack('H*', sha1(pack('H*', $nonce) . pack('a*', $created) . pack('a*', $pass))));
$auth = new stdClass();
$auth->Username = new SoapVar($user, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Password = new SoapVar($pass, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Nonce = new SoapVar($passdigest, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Created = new SoapVar($created, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wsu_ns);
$username_token = new stdClass();
$username_token->UsernameToken = new SoapVar($auth, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns);
$security_sv = new SoapVar(
new SoapVar($username_token, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns),
SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'Security', $this->wss_ns);
parent::__construct($this->wss_ns, 'Security', $security_sv, true);
}
}
сравните также с данными, приведенными в ответе fooobar.com/questions/183633/...
Ответ 3
Для защиты дайджеста паролем вы можете использовать следующее:
/**
* This function implements a WS-Security digest authentification for PHP.
*
* @access private
* @param string $user
* @param string $password
* @return SoapHeader
*/
function soapClientWSSecurityHeader($user, $password)
{
// Creating date using yyyy-mm-ddThh:mm:ssZ format
$tm_created = gmdate('Y-m-d\TH:i:s\Z');
$tm_expires = gmdate('Y-m-d\TH:i:s\Z', gmdate('U') + 180); //only necessary if using the timestamp element
// Generating and encoding a random number
$simple_nonce = mt_rand();
$encoded_nonce = base64_encode($simple_nonce);
// Compiling WSS string
$passdigest = base64_encode(sha1($simple_nonce . $tm_created . $password, true));
// Initializing namespaces
$ns_wsse = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd';
$ns_wsu = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd';
$password_type = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest';
$encoding_type = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary';
// Creating WSS identification header using SimpleXML
$root = new SimpleXMLElement('<root/>');
$security = $root->addChild('wsse:Security', null, $ns_wsse);
//the timestamp element is not required by all servers
$timestamp = $security->addChild('wsu:Timestamp', null, $ns_wsu);
$timestamp->addAttribute('wsu:Id', 'Timestamp-28');
$timestamp->addChild('wsu:Created', $tm_created, $ns_wsu);
$timestamp->addChild('wsu:Expires', $tm_expires, $ns_wsu);
$usernameToken = $security->addChild('wsse:UsernameToken', null, $ns_wsse);
$usernameToken->addChild('wsse:Username', $user, $ns_wsse);
$usernameToken->addChild('wsse:Password', $passdigest, $ns_wsse)->addAttribute('Type', $password_type);
$usernameToken->addChild('wsse:Nonce', $encoded_nonce, $ns_wsse)->addAttribute('EncodingType', $encoding_type);
$usernameToken->addChild('wsu:Created', $tm_created, $ns_wsu);
// Recovering XML value from that object
$root->registerXPathNamespace('wsse', $ns_wsse);
$full = $root->xpath('/root/wsse:Security');
$auth = $full[0]->asXML();
return new SoapHeader($ns_wsse, 'Security', new SoapVar($auth, XSD_ANYXML), true);
}
Чтобы использовать его с PHP SoapClient, используйте этот способ:
$client = new SoapClient('http://endpoint');
$client->__setSoapHeaders(soapClientWSSecurityHeader('myUser', 'myPassword'));
// $client->myService(array('param' => 'value', ...);
Ответ 4
У меня есть более простое решение, чем расширение существующей библиотеки soapclient.
Шаг 1. Создайте два класса для создания структуры для заголовков WSSE.
class clsWSSEAuth {
private $Username;
private $Password;
function __construct($username, $password) {
$this->Username=$username;
$this->Password=$password;
}
}
class clsWSSEToken {
private $UsernameToken;
function __construct ($innerVal){
$this->UsernameToken = $innerVal;
}
}
Шаг 2. Создание переменных мыла для имени пользователя и пароля
$username = 1111;
$password = 1111;
//Check with your provider which security name-space they are using.
$strWSSENS = "http://schemas.xmlsoap.org/ws/2002/07/secext";
$objSoapVarUser = new SoapVar($username, XSD_STRING, NULL, $strWSSENS, NULL, $strWSSENS);
$objSoapVarPass = new SoapVar($password, XSD_STRING, NULL, $strWSSENS, NULL, $strWSSENS);
Шаг 3. Создайте объект для класса Auth и перейдите в мыло var
$objWSSEAuth = new clsWSSEAuth($objSoapVarUser, $objSoapVarPass);
Шаг4: Создайте SoapVar из объекта класса Auth
$objSoapVarWSSEAuth = new SoapVar($objWSSEAuth, SOAP_ENC_OBJECT, NULL, $strWSSENS, 'UsernameToken', $strWSSENS);
Шаг 5. Создание объекта для класса токенов
$objWSSEToken = new clsWSSEToken($objSoapVarWSSEAuth);
Шаг6: Создайте SoapVar из объекта класса Token
$objSoapVarWSSEToken = new SoapVar($objWSSEToken, SOAP_ENC_OBJECT, NULL, $strWSSENS, 'UsernameToken', $strWSSENS);
Шаг7: Создайте SoapVar для "Безопасность" node
$objSoapVarHeaderVal=new SoapVar($objSoapVarWSSEToken, SOAP_ENC_OBJECT, NULL, $strWSSENS, 'Security', $strWSSENS);
Шаг 8: Создайте объект заголовка вне безопасности soapvar
$objSoapVarWSSEHeader = new SoapHeader($strWSSENS, 'Security', $objSoapVarHeaderVal,true, 'http://abce.com');
//Third parameter here makes 'mustUnderstand=1
//Forth parameter generates 'actor="http://abce.com"'
Шаг 9. Создание объекта клиента Soap
$objClient = new SoapClient($WSDL, $arrOptions);
Шаг 10: Установите заголовки для объекта soapclient
$objClient->__setSoapHeaders(array($objSoapVarWSSEHeader));
Шаг 11: Заключительный вызов метода
$objResponse = $objClient->__soapCall($strMethod, $requestPayloadString);
Ответ 5
$client = new SoapClient("some.wsdl", array('login' => "some_name",
'password' => "some_password"));
Из документации php
Ответ 6
Я принял отличное решение Alain Tiemblo, но я использую пароль, а не дайджест.
/**
* This function implements a WS-Security authentication for PHP.
*
* @access private
* @param string $user
* @param string $password
* @return SoapHeader
*/
function soapClientWSSecurityHeader($user, $password)
{
// Creating date using yyyy-mm-ddThh:mm:ssZ format
$tm_created = gmdate('Y-m-d\TH:i:s\Z');
$tm_expires = gmdate('Y-m-d\TH:i:s\Z', gmdate('U') + 180); //only necessary if using the timestamp element
// Generating and encoding a random number
$simple_nonce = mt_rand();
$encoded_nonce = base64_encode($simple_nonce);
// Compiling WSS string
$passdigest = base64_encode(sha1($simple_nonce . $tm_created . $password, true));
// Initializing namespaces
$ns_wsse = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd';
$ns_wsu = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd';
$password_type = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText';
$encoding_type = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary';
// Creating WSS identification header using SimpleXML
$root = new SimpleXMLElement('<root/>');
$security = $root->addChild('wsse:Security', null, $ns_wsse);
//the timestamp element is not required by all servers
$timestamp = $security->addChild('wsu:Timestamp', null, $ns_wsu);
$timestamp->addAttribute('wsu:Id', 'Timestamp-28');
$timestamp->addChild('wsu:Created', $tm_created, $ns_wsu);
$timestamp->addChild('wsu:Expires', $tm_expires, $ns_wsu);
$usernameToken = $security->addChild('wsse:UsernameToken', null, $ns_wsse);
$usernameToken->addChild('wsse:Username', $user, $ns_wsse);
$usernameToken->addChild('wsse:Password', $password, $ns_wsse)->addAttribute('Type', $password_type);
$usernameToken->addChild('wsse:Nonce', $encoded_nonce, $ns_wsse)->addAttribute('EncodingType', $encoding_type);
$usernameToken->addChild('wsu:Created', $tm_created, $ns_wsu);
// Recovering XML value from that object
$root->registerXPathNamespace('wsse', $ns_wsse);
$full = $root->xpath('/root/wsse:Security');
$auth = $full[0]->asXML();
return new SoapHeader($ns_wsse, 'Security', new SoapVar($auth, XSD_ANYXML), true);
}
Чтобы вызвать его, используйте
$client = new SoapClient('YOUR ENDPOINT');
$userid = "userid";
$password = "password";
$client->__setSoapHeaders(soapClientWSSecurityHeader($userid,$password));
Ответ 7
WS Secure с паролем дайджеста. Этот код работает для меня:
class WsseAuthHeader extends SoapHeader {
private $wss_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd';
private $wsu_ns = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd';
private $type_password_digest= 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest';
private $type_password_text= 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText';
private $encoding_type_base64 = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary';
private function authText($user, $pass) {
$auth = new stdClass();
$auth->Username = new SoapVar($user, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Password = new SoapVar('<ns2:Password Type="'.$this->type_password_text.'">' . $pass . '</ns2:Password>', XSD_ANYXML );
return $auth;
}
private function authDigest($user, $pass) {
$created = gmdate('Y-m-d\TH:i:s\Z');
$nonce = mt_rand();
$enpass = base64_encode(pack('H*', sha1(pack('H*', $nonce) . pack('a*', $created) . pack('a*', $pass))));
$auth = new stdClass();
$auth->Username = new SoapVar($user, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wss_ns);
$auth->Password = new SoapVar('<ns2:Password Type="'.$this->type_password_digest.'">' . $enpass . '</ns2:Password>', XSD_ANYXML );
$auth->Nonce = new SoapVar('<ns2:Nonce EncodingType="' . $this->encoding_type_base64 . '">' . base64_encode(pack('H*', $nonce)) . '</ns2:Nonce>', XSD_ANYXML);
$auth->Created = new SoapVar($created, XSD_STRING, NULL, $this->wss_ns, NULL, $this->wsu_ns);
return $auth;
}
function __construct($user, $pass, $useDigest=true) {
if ($useDigest) {
$auth = $this->authDigest($user, $pass);
}else{
$auth = $this->authText($user, $pass);
}
$username_token = new stdClass();
$username_token->UsernameToken = new SoapVar($auth, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns);
$security_sv = new SoapVar(
new SoapVar($username_token, SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'UsernameToken', $this->wss_ns),
SOAP_ENC_OBJECT, NULL, $this->wss_ns, 'Security', $this->wss_ns);
parent::__construct($this->wss_ns, 'Security', $security_sv, true);
}
}
Использование:
$client->__setSoapHeaders([new WsseAuthHeader($login, $password)]);