用PHP连接到受WS-Security保护的Web服务

我正在尝试连接到受密码保护的Web服务,并且URL是https。 我无法弄清楚如何在脚本发出请求之前进行身份validation。 它似乎是一旦我定义的服务提出请求。 例如,如果我把:

$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内置的类来设置它。 我尝试添加“用户名”和“密码”的数组,但这是不好的。 问题是,我甚至无法知道我是否到达远程站点,更不用说是否拒绝了这个请求。

问题似乎是,WSDL文档被某种程度的保护(基本authentication – 我不认为SoapClient支持摘要authentication,所以在这种情况下你会不走运), SoapClient因此无法读取和parsing服务描述。

首先,您应该尝试在浏览器中打开WSDL位置,以检查是否提供了validation对话框。 如果存在authentication对话框,则必须确保SoapClient在检索WSDL文档时使用所需的login凭证。 问题在于SoapClient只会在调用服务时发送loginpassword选项(以及使用证书身份validation时的local_cert选项)给出的凭证,而不是在获取WSDL时发送(请参见此处 )。 有两种方法可以解决这个问题:

  1. 将login凭据添加到SoapClient构造函数调用的WSDL URL

     $client = new SoapClient( 'https://' . urlencode($login) . ':' . urlencode($password) . '@example.com/WSDL/nameofservice', array( 'login' => $login, 'password' => $password ) ); 

    这应该是最简单的解决scheme – 但在PHP 错误#27777写道,这也不会工作(我没有尝试过)。

  2. 使用HTTPstream包装器或ext/curl手动获取WSDL,或者通过浏览器或wget手动获取WSDL,将其存储在磁盘上并通过引用本地WSDL实例化SoapClient

    如果WSDL文档发生更改,则此解决scheme可能会有问题,因为您必须检测更改并将新版本存储在磁盘上。

如果没有显示validation对话框,并且您可以在浏览器中读取WSDL,则应该提供更多的细节来检查其他可能的错误/问题。

这个问题与服务本身无关,因为SoapClient扼stream器在发送服务本身的调用之前已经阅读了服务描述文档。

编辑:

让WSDL文件在本地是第一步 – 这将允许SoapClient知道如何与服务进行通信。 无论WSDL是直接从服务位置,从另一个服务器还是从本地文件读取 – 服务URL都在WSDL中编码, SoapClient总是知道在哪里查找服务端点。

现在的第二个问题是SoapClient不支持WS-Security规范,这意味着您必须扩展SoapClient来处理特定的头文件。 添加所需行为的扩展点是SoapClient::__doRequest() ,它将XML负载发送到服务端点之前进行预处理。 但是我认为自己实施WS-Security解决scheme需要对WS-Security规范有相当的了解。 也许也可以使用SoapClient::__setSoapHeaders()和相应的SoapHeader创buildWS-Security头并将其打包到XML请求中,但是我怀疑这会起作用,只剩下自定义的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 eg be loaded into a DomDocument to make it modifiable. */ $domRequest = new DOMDocument(); $domRequest->loadXML($request); // modify XML using the DOM API, eg 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身份validation,您必须将以下内容添加到SOAP-header中:

 <wsse:UsernameToken> <wsse:Username>userid</wsse:Username> <wsse:Password>password</wsse:Password> </wsse:UsernameToken> 

但正如我上面所说:我认为需要更多关于WS-Security规范和给定服务体系结构的知识才能实现这一点。

如果您需要针对整个WS- *规范范围的企业级解决scheme,并且如果您可以安装PHP模块,则应该查看适用于PHP的WSO2 Web服务框架(WSO2 WSF / PHP)

简单地扩展SoapHeader来创build一个Wsse编译器authentication:

 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('Ymd\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); } } 

请与https://stackoverflow.com/a/18575154/367456中提供的详细信息进行比较;

对于密码摘要安全性,您可以使用以下内容:

  /** * 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('Ymd\TH:i:s\Z'); $tm_expires = gmdate('Ymd\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', ...); 

我比扩展现有的soapclient库有更简单的解决scheme。

第一步:创build两个类来为WSSE头创build一个结构

 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; } } 

第二步:为用户名和密码创build肥皂variables

 $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); 

Step3:为Auth Class创build对象并传入soap var

 $objWSSEAuth = new clsWSSEAuth($objSoapVarUser, $objSoapVarPass); 

第四步:创buildSoapVar作为Auth类的对象

 $objSoapVarWSSEAuth = new SoapVar($objWSSEAuth, SOAP_ENC_OBJECT, NULL, $strWSSENS, 'UsernameToken', $strWSSENS); 

第五步:为令牌类创build对象

 $objWSSEToken = new clsWSSEToken($objSoapVarWSSEAuth); 

Step6:从Token类的对象中创buildSoapVar

 $objSoapVarWSSEToken = new SoapVar($objWSSEToken, SOAP_ENC_OBJECT, NULL, $strWSSENS, 'UsernameToken', $strWSSENS); 

第七步:为“安全”节点创buildSoapVar

 $objSoapVarHeaderVal=new SoapVar($objSoapVarWSSEToken, SOAP_ENC_OBJECT, NULL, $strWSSENS, 'Security', $strWSSENS); 

Step8:创build安全soapvar的头对象

 $objSoapVarWSSEHeader = new SoapHeader($strWSSENS, 'Security', $objSoapVarHeaderVal,true, 'http://abce.com'); //Third parameter here makes 'mustUnderstand=1 //Forth parameter generates 'actor="http://abce.com"' 

第九步:创buildSoap客户端的对象

 $objClient = new SoapClient($WSDL, $arrOptions); 

第10步:为soapclient对象设置标题

 $objClient->__setSoapHeaders(array($objSoapVarWSSEHeader)); 

步骤11:最后的方法调用

 $objResponse = $objClient->__soapCall($strMethod, $requestPayloadString); 
 $client = new SoapClient("some.wsdl", array('login' => "some_name", 'password' => "some_password")); 

从PHP文档

我采用了Alain Tiemblo的优秀解决scheme,但我使用密码而不是摘要。

  /** * 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('Ymd\TH:i:s\Z'); $tm_expires = gmdate('Ymd\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)); 

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('Ymd\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)]);