简体   繁体   English

无法使用PHP SOAP扩展使用WS-Security连接到SSL Web服务-证书,复杂的WSDL

[英]Can't connect to SSL web service with WS-Security using PHP SOAP extension - certificate, complex WSDL

Using the PHP5 SOAP extension I have been unable to connect to a web service having an https endpoint, with client certificate and using WS-Security, although I can connect using soapUI with the exact same wsdl and client certificate, and obtain the normal response to the request. 使用PHP5 SOAP扩展,我无法使用客户端证书和WS-Security连接到具有https端点的Web服务,尽管我可以使用具有完全相同的wsdl和客户端证书的soapUI进行连接,并获得对请求。 There is no HTTP authentication and no proxy is involved. 没有HTTP身份验证,也没有代理。 The message I get is 'Could not connect to host'. 我收到的消息是“无法连接到主机”。 Have been able to verify that I am NOT hitting the host server. 能够验证我没有击中主机服务器。 (Earlier I wrongly said that I was hitting the server.) (以前我错误地说我在打服务器。)

The self-signed client SSL certificate is a .pem file converted by openssl from a .p12 keystore which in turn was converted by keytool from a .jks keystore having a single entry consisting of private key and client certificate. 自签名客户端SSL证书是一个.pem文件,由openssl从.p12密钥库转换而来,而该文件又由keytool从.jks密钥库转换而来,该.jks密钥库具有一个包含私钥和客户端证书的条目。

In soapUI I did not need to supply a server private certificate, the only two files I gave it were the wdsl and pem. 在soapUI中,我不需要提供服务器专用证书,我给它的仅有的两个文件是wdsl和pem。 I did have to supply the pem and its passphrase to be able to connect. 我确实必须提供pem及其密码才能连接。 I am speculating that despite the error message my problem might actually be in the formation of the XML request rather than the SSL connection itself. 我推测尽管有错误消息,但我的问题实际上可能是XML请求而不是SSL连接本身形成的。

The wsdl I have been given has nested complex types. 我得到的wsdl具有嵌套的复杂类型。 The php server is on my Windows XP laptop with IIS. php服务器在带有IIS的Windows XP笔记本电脑上。

The code, data values and WSDL extracts are shown below. 代码,数据值和WSDL提取如下所示。 (The WSSoapClient class simply extends SoapClient, adding a WS-Security Username Token header with mustUnderstand = true and including a nonce, both of which the soapUI call had required.) (WSSoapClient类简单地扩展了SoapClient,添加了一个具有mustUnderstand = true的WS-Security用户名令牌标头,并包含一个现成的值,两个都需要soapUI调用。)

Would so much appreciate any help. 非常感谢您的帮助。 I'm a newbie thrown in at the deep end, and how! 我是一个新手,怎么了! Have done vast amounts of Googling on this over many days, following many suggestions and have read Pro PHP by Kevin McArthur. 经过许多建议,经过许多天的大量Google搜寻,并阅读了Kevin McArthur撰写的Pro PHP。 An attempt to use classmaps in place of nested arrays also fell flat. 使用类映射代替嵌套数组的尝试也失败了。


The Code 编码

class STEeService
{


public function invokeWebService(array $connection, $operation, array $request)
 {
  try
   {  
    $localCertificateFilespec = $connection['localCertificateFilespec'];
$localCertificatePassphrase = $connection['localCertificatePassphrase'];

$sslOptions = array(
   'ssl' => array(
     'local_cert' => $localCertificateFilespec,
     'passphrase' => $localCertificatePassphrase,
     'allow_self-signed' => true,
     'verify_peer' => false
             )
          );  
$sslContext = stream_context_create($sslOptions);

$clientArguments = array(
    'stream_context' => $sslContext,
    'local_cert' => $localCertificateFilespec,    
    'passphrase' => $localCertificatePassphrase,
    'trace' => true,
    'exceptions' => true,   
    'encoding' => 'UTF-8',
    'soap_version' => SOAP_1_1
   );

$oClient = new WSSoapClient($connection['wsdlFilespec'], $clientArguments); 
$oClient->__setUsernameToken($connection['username'], $connection['password']);        

   return $oClient->__soapCall($operation, $request);      
   }
   catch (exception $e)
   {
    throw new Exception("Exception in eServices " . $operation . " ," . $e->getMessage(), "\n");
   }

 }
}

$connection is as follows: $ connection如下:

array(5) { ["username"]=> string(8) "DFU00050" 
["password"]=> string(10) "Fabricate1" 
["wsdlFilespec"]=> 
string (63) "c:/inetpub/wwwroot/DMZExternalService_Concrete_WSDL_Staging.xml" 
["localCertificateFilespec"]=> string(37) 
"c:/inetpub/wwwroot/ClientKeystore.pem"
["localCertificatePassphrase"]=> string(14) "password123456" }

$clientArguments is as follows: $ clientArguments如下:

array(7) { ["stream_context"]=> resource(8) of type (stream-context) 
["local_cert"]=> string(37) "c:/inetpub/wwwroot/ClientKeystore.pem" 
["passphrase"]=> string(14) "password123456" 
["trace"]=> bool(true) ["exceptions"]=> bool(true) ["encoding"]=> string(5) "UTF-8" 
["soap_version"]=> int(1) }

$operation is as follows: $ operation如下:

'getConsignmentDetails'

$request is as follows: $ request如下:

array(1) { [0]=> array(2) { ["header"]=> array(2) { 
["source"]=> string(9) "customerA" ["accountNo"]=> string(8) "10072906" } 
["consignmentId"]=> string(11) "GKQ00000085" } }

Note how there is an extra level of nesting, an array wrapping the request which is itself an array. 请注意 ,嵌套是如何进行的,嵌套是一个包装请求的数组,它本身就是一个数组。 This was suggested in a post although I don't see the reason, but it seems to help avoid other exceptions. 尽管我不知道原因,但在帖子中建议这样做,但这似乎有助于避免其他异常。


The exception thrown by ___soapCall is as follows: ___soapCall引发的异常如下:

    object(SoapFault)#6 (9) { ["message":protected]=> 
string(25) "Could not connect to host" ["string":"Exception":private]=> string(0) "" 
    ["code":protected]=> int(0) ["file":protected]=> string(43) "C:\Inetpub\wwwroot\eServices\WSSecurity.php" 
    ["line":protected]=> int(85) ["trace":"Exception":private]=> array(5) { [0]=> array(6) { 
    ["file"]=> string(43) "C:\Inetpub\wwwroot\eServices\WSSecurity.php" ["line"]=> int(85) ["function"]=> string(11) "__doRequest" 
    ["class"]=> string(10) "SoapClient" ["type"]=> string(2) "->" ["args"]=> array(4) { 
    [0]=> string(1240) " DFU00050 Fabricate1 E0ByMUA= 2010-10-28T13:13:52Z customerA10072906GKQ00000085 " 
    [1]=> string(127) "https://services.startrackexpress.com.au:7560/DMZExternalService/InterfaceServices/ExternalOps.serviceagent/OperationsEndpoint1" 
    [2]=> string(104) "/DMZExternalService/InterfaceServices/ExternalOps.serviceagent/OperationsEndpoint1/getConsignmentDetails" [3]=> int(1) } } 
    [1]=> array(4) { ["function"]=> string(11) "__doRequest" ["class"]=> string(39) "startrackexpress\eservices\WSSoapClient" 
    ["type"]=> string(2) "->" ["args"]=> array(5) { [0]=> string(1240) " DFU00050 Fabricate1 E0ByMUA= 2010-10-28T13:13:52Z customerA10072906GKQ00000085 " 
    [1]=> string(127) "https://services.startrackexpress.com.au:7560/DMZExternalService/InterfaceServices/ExternalOps.serviceagent/OperationsEndpoint1" 
    [2]=> string(104) "/DMZExternalService/InterfaceServices/ExternalOps.serviceagent/OperationsEndpoint1/getConsignmentDetails" [3]=> int(1) [4]=> int(0) } }
    [2]=> array(6) { ["file"]=> string(43) "C:\Inetpub\wwwroot\eServices\WSSecurity.php" ["line"]=> int(70) ["function"]=> string(10) "__soapCall" 
    ["class"]=> string(10) "SoapClient" ["type"]=> string(2) "->" ["args"]=> array(4) { [0]=> string(21) "getConsignmentDetails" [1]=> array(1) { 
    [0]=> array(2) { ["header"]=> array(2) { ["source"]=> string(9) "customerA" ["accountNo"]=> string(8) "10072906" } 
    ["consignmentId"]=> string(11) "GKQ00000085" } } [2]=> NULL [3]=> object(SoapHeader)#5 (4) { 
    ["namespace"]=> string(81) "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" ["name"]=> string(8) "Security" 
    ["data"]=> object(SoapVar)#4 (2) { ["enc_type"]=> int(147) ["enc_value"]=> string(594) " DFU00050 Fabricate1 E0ByMUA= 2010-10-28T13:13:52Z " } 
    ["mustUnderstand"]=> bool(true) } } } [3]=> array(6) { ["file"]=> string(42) "C:\Inetpub\wwwroot\eServices\eServices.php" 
    ["line"]=> int(87) ["function"]=> string(10) "__soapCall" ["class"]=> string(39) "startrackexpress\eservices\WSSoapClient" 
    ["type"]=> string(2) "->" ["args"]=> array(2) { [0]=> string(21) "getConsignmentDetails" [1]=> array(1) { [0]=> array(2) { 
    ["header"]=> array(2) { ["source"]=> string(9) "customerA" ["accountNo"]=> string(8) "10072906" } ["consignmentId"]=> string(11) "GKQ00000085" } } } } 
    [4]=> array(6) { ["file"]=> string(58) "C:\Inetpub\wwwroot\eServices\EnquireConsignmentDetails.php" ["line"]=> int(44) 
    ["function"]=> string(16) "invokeWebService" ["class"]=> string(38) "startrackexpress\eservices\STEeService" ["type"]=> string(2) "->" 
    ["args"]=> array(3) { [0]=> array(5) { ["username"]=> string(10) "DFU00050 " ["password"]=> string(12) "Fabricate1 " 
    ["wsdlFilespec"]=> string(63) "c:/inetpub/wwwroot/DMZExternalService_Concrete_WSDL_Staging.xml" 
    ["localCertificateFilespec"]=> string(37) "c:/inetpub/wwwroot/ClientKeystore.pem" ["localCertificatePassphrase"]=> string(14) "password123456" } 
    [1]=> string(21) "getConsignmentDetails" [2]=> array(1) { [0]=> array(2) { ["header"]=> array(2) { ["source"]=> string(9) "customerA" 
    ["accountNo"]=> string(8) "10072906" } ["consignmentId"]=> string(11) "GKQ00000085" } } } } } 
    ["previous":"Exception":private]=> NULL ["faultstring"]=> string(25) "Could not connect to host" ["faultcode"]=> string(4) "HTTP" }

Here are some WSDL extracts (TIBCO BusinessWorks): 以下是一些WSDL摘录(TIBCO BusinessWorks):

            <xsd:complexType name="TransactionHeaderType">
            <xsd:sequence>
                <xsd:element name="source" type="xsd:string"/>
                <xsd:element name="accountNo" type="xsd:integer"/>
                <xsd:element name="userId" type="xsd:string" minOccurs="0"/>
                <xsd:element name="transactionId" type="xsd:string" minOccurs="0"/>
                <xsd:element name="transactionDatetime" type="xsd:dateTime" minOccurs="0"/>
            </xsd:sequence>
        </xsd:complexType>

       <xsd:element name="getConsignmentDetailRequest">
            <xsd:complexType>
                <xsd:sequence>
                    <xsd:element name="header" type="prim:TransactionHeaderType"/>
                    <xsd:element name="consignmentId" type="prim:ID" maxOccurs="unbounded"/>
                </xsd:sequence>
            </xsd:complexType>
        </xsd:element>
        <xsd:element name="getConsignmentDetailResponse">
            <xsd:complexType>
                <xsd:sequence>
                    <xsd:element name="consignment" type="freight:consignmentType" minOccurs="0" maxOccurs="unbounded"/>
                </xsd:sequence>
            </xsd:complexType>
        </xsd:element>

        <xsd:element name="getConsignmentDetailRequest">
            <xsd:complexType>
                <xsd:sequence>
                    <xsd:element name="header" type="prim:TransactionHeaderType"/>
                    <xsd:element name="consignmentId" type="prim:ID" maxOccurs="unbounded"/>
                </xsd:sequence>
            </xsd:complexType>
        </xsd:element>
        <xsd:element name="getConsignmentDetailResponse">
            <xsd:complexType>
                <xsd:sequence>
                    <xsd:element name="consignment" type="freight:consignmentType" minOccurs="0" maxOccurs="unbounded"/>
                </xsd:sequence>
            </xsd:complexType>
        </xsd:element>

    <wsdl:operation name="getConsignmentDetails">
        <wsdl:input message="tns:getConsignmentDetailsRequest"/>
        <wsdl:output message="tns:getConsignmentDetailsResponse"/>
        <wsdl:fault name="fault1" message="tns:fault"/>
    </wsdl:operation>

<wsdl:service name="ExternalOps">
    <wsdl:port name="OperationsEndpoint1" binding="tns:OperationsEndpoint1Binding">
        <soap:address location="https://services.startrackexpress.com.au:7560/DMZExternalService/InterfaceServices/ExternalOps.serviceagent/OperationsEndpoint1"/>
    </wsdl:port>
</wsdl:service>

And here in case it's relevant is the WSSoapClient class: 如果相关,这里是WSSoapClient类:

    <?PHP
namespace startrackexpress\eservices;
use SoapClient, SoapVar, SoapHeader;

class WSSoapClient extends SoapClient
{
 private $username;
 private $password;

/*Generates a WS-Security header*/
 private function wssecurity_header()
 {
  $timestamp = gmdate('Y-m-d\TH:i:s\Z');
  $nonce = mt_rand(); 
  $passdigest = base64_encode(pack('H*', sha1(pack('H*', $nonce).pack('a*', $timestamp).pack('a*', $this->password))));

  $auth = '
<wsse:Security SOAP-ENV:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
    <wsse:Username>' . $this->username . '</wsse:Username>
    <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">' . 
 $this->password . '</wsse:Password>
    <wsse:Nonce>' . base64_encode(pack('H*', $nonce)).'</wsse:Nonce>
    <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' . $timestamp . '</wsu:Created>
   </wsse:UsernameToken>
</wsse:Security>
';
  $authvalues = new SoapVar($auth, XSD_ANYXML); 
  $header = new SoapHeader("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security",$authvalues, true);

  return $header;
 }

 // Sets a username and passphrase
 public function __setUsernameToken($username,$password)
 {
  $this->username=$username;
  $this->password=$password;
 }

 // Overwrites the original method, adding the security header
 public function __soapCall($function_name, $arguments, $options=null, $input_headers=null, $output_headers=null)
 {
  try
  {
    $result = parent::__soapCall($function_name, $arguments, $options, $this->wssecurity_header());
    return $result;
  }
  catch (exception $e)
  {
   throw new Exception("Exception in __soapCall, " . $e->getMessage(), "\n");
  }
 }
}
?>

Update: 更新:

The request XML would have been as follows: 请求XML将如下所示:

<?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://startrackexpress/Common/Primitives/v1" xmlns:ns2="http://startrackexpress/Common/actions/externals/Consignment/v1" xmlns:ns3="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    <SOAP-ENV:Header> <wsse:Security SOAP-ENV:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> 
<wsse:UsernameToken> <wsse:Username>DFU00050</wsse:Username> 
    <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">Fabricate1</wsse:Password> 
    <wsse:Nonce>M4FIeGA=</wsse:Nonce> 
    <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2010-10-29T14:05:27Z</wsu:Created> 
    </wsse:UsernameToken> 
    </wsse:Security> </SOAP-ENV:Header>
    <SOAP-ENV:Body><ns2:getConsignmentDetailRequest>
    <ns2:header><ns1:source>customerA</ns1:source><ns1:accountNo>10072906</ns1:accountNo></ns2:header>
    <ns2:consignmentId>GKQ00000085</ns2:consignmentId>
    </ns2:getConsignmentDetailRequest></SOAP-ENV:Body>
    </SOAP-ENV:Envelope>

This was obtained with the following code in WSSoapClient: 这是通过WSSoapClient中的以下代码获得的:

public function __doRequest($request, $location, $action, $version)         {
    echo "<p> " . htmlspecialchars($request) . " </p>" ;    
    return parent::__doRequest($request, $location, $action, $version);
}

For anyone else struggling with the startrack API. 对于任何其他使用startrack API的人。 Here is class I wrote to go via CURL instead. 这是我编写的通过CURL代替的课程。

Instructions: 

Add the attached file to:
Client/Executables 

Change line 28 from 

class WSSoapClient extends SoapClient

To:

require('SoapClientCurl.class.php');
class WSSoapClient extends SoapClientCurl

<?php

/**
 * Override to overcome problems with Startrack Self Signed SSL Certificates on
 * certain server configurations.
 *
 * The important options here that aren't available in the SoapClient options are
 * CURLOPT_SSLVERSION       - Forces the SSl Version to 3
 * CURLOPT_SSL_VERIFYHOST   - Tells ssl not to care that the Startrack SSL certificate is for a different domain
 * CURLOPT_SSL_VERIFYPEER   - Tells ssl not to care that the Startrack SSL certificate is from a bogus CA (I think)
 *
 */
class SoapClientCurl extends SoapClient
{
    /**
     *
     * @param string $request       - The XML SOAP request.
     * @param string $location      - The URL to request.
     * @param string $action        - The SOAP action.
     * @param int $version          - The SOAP version.
     * @param boolean $one_way      - If one_way is set to 1, this method returns nothing. Use this where a response is not expected.
     * @throws SoapFault
     * @return string|void
     */
    public function __doRequest($request, $location, $action, $version, $one_way = 0)
    {
        $handle = curl_init();

        curl_setopt($handle, CURLOPT_URL, $location);
        curl_setopt($handle, CURLOPT_HTTPHEADER, array(
                'Content-type: text/xml;charset="utf-8"',
                'Accept: text/xml',
                'Cache-Control: no-cache',
                'Pragma: no-cache',
                'SOAPAction: '.$action,
                'Content-length: '.strlen($request))
        );

        curl_setopt($handle, CURLOPT_RETURNTRANSFER,    true);
        curl_setopt($handle, CURLOPT_POSTFIELDS,        $request);
        curl_setopt($handle, CURLOPT_SSLVERSION,        3);
        curl_setopt($handle, CURLOPT_PORT,              443);
        curl_setopt($handle, CURLOPT_POST,              true );
        curl_setopt($handle, CURLOPT_SSL_VERIFYHOST,    false);
        curl_setopt($handle, CURLOPT_SSL_VERIFYPEER,    false);

        $response = curl_exec($handle);

        if(empty($response))
        {
            throw new SoapFault('CURL error: '.curl_error($handle), curl_errno($handle));
        }

        curl_close($handle);

        if(1 !== $one_way)
        {
            return $response;
        }
    }
}

When I ran the code under Apache (using XAMPP) it worked first time. 当我在Apache(使用XAMPP)下运行代码时,它第一次起作用。 My problem must have been somewhere in the configuration of IIS or PHP. 我的问题一定是在IIS或PHP的配置中。

There was one typo but it wasn't the showstopper: 有一个错别字,但不是排头兵:

'allow_self-signed' => true

should be 应该

'allow_self_signed' => true

Given the error message you've got AND the fact that you're hitting the target server, I'd guess that the SSL client certificate is screwing things up. 考虑到您收到的错误消息以及您正在攻击目标服务器的事实,我猜想SSL客户端证书正在搞砸了。 I notice you're specifying this twice - once directly in the SOAP client params and once in the stream context - is this necessary? 我注意到您指定了两次(一次直接在SOAP客户端参数中,一次在流上下文中),是否有必要? Have you tried omitting the stream context and using just the SoapClient params? 您是否尝试过省略流上下文并仅使用SoapClient参数? Do you have to use a client certificate? 必须使用客户证书吗?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM