用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
只会在调用服务时发送login
和password
选项(以及使用证书身份validation时的local_cert
选项)给出的凭证,而不是在获取WSDL时发送(请参见此处 )。 有两种方法可以解决这个问题:
-
将login凭据添加到
SoapClient
构造函数调用的WSDL URL$client = new SoapClient( 'https://' . urlencode($login) . ':' . urlencode($password) . '@example.com/WSDL/nameofservice', array( 'login' => $login, 'password' => $password ) );
这应该是最简单的解决scheme – 但在PHP 错误#27777写道,这也不会工作(我没有尝试过)。
-
使用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); } }
对于密码摘要安全性,您可以使用以下内容:
/** * 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)]);