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

时间:2009-06-04 23:38:52

标签: php web-services soap ws-security

我正在尝试连接到受密码保护且网址为https的Web服务。在脚本发出请求之前,我无法弄清楚如何进行身份验证。它似乎在我定义服务时立即发出请求。例如,如果我输入:

$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

如果我尝试将服务定义为肥皂服务器,例如:

$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”添加到数组中,但这并不好。问题是我甚至无法判断我是否到达了远程站点,更不用说它是否拒绝了请求。

7 个答案:

答案 0 :(得分:34)

只需扩展SoapHeader即可创建Wsse compilant身份验证:

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

与答案https://stackoverflow.com/a/18575154/367456

中给出的详细信息进行比较

答案 1 :(得分:28)

问题似乎是WSDL文档受到某种程度的保护(基本身份验证 - 我不认为SoapClient支持摘要身份验证,所以你在这种情况下运气不好)因此SoapClient无法读取和解析服务描述。

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

  1. SoapClient构造函数调用

    上将登录凭据添加到WSDL网址
    $client = new SoapClient(
        'https://' . urlencode($login) . ':' . urlencode($password) . '@example.com/WSDL/nameofservice',
        array(
            'login' => $login,
            'password' => $password
        )
    );
    

    这应该是最简单的解决方案 - 但在PHP Bug #27777中写道,这也不起作用(我没试过)。

  2. 使用HTTP流包装器或ext/curl手动获取WSDL,或者通过浏览器或wget手动获取WSDL,将其存储在磁盘上并使用以下内容实例化SoapClient对本地WSDL的引用。

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

  3. 如果没有显示身份验证对话框,并且您可以在浏览器中阅读WSDL,则应提供更多详细信息以检查其他可能的错误/问题。

    这个问题与服务本身无关,因为SoapClient在发出对服务本身的调用之前已经在读取服务描述文档时发生了窒息。

    修改

    在本地拥有WSDL文件是第一步 - 这将允许SoapClient知道如何与服务进行通信。无论WSDL是直接从服务位置,从另一个服务器提供还是从本地文件读取都无关紧要 - 服务URL在WSDL中编码,因此SoapClient始终知道在哪里查找服务端点。

    现在的第二个问题是SoapClient本身不支持WS-Security规范,这意味着您必须扩展SoapClient来处理特定的标头。添加所需行为的扩展点是SoapClient::__doRequest(),它在将XML有效负载发送到服务端点之前对其进行预处理。但我认为自己实施WS-Security解决方案需要对特定的WS-Security规范有一定的了解。也许可以使用SoapClient::__setSoapHeaders()和相应的SoapHeader创建WS-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 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 for PHP (WSO2 WSF/PHP)

答案 2 :(得分:17)

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

   /**
    * 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', ...);

答案 3 :(得分:6)

我提供的解决方案比扩展现有的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类创建对象并传入soap var

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

Step4:从Auth类的对象

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

Step5:为Token Class创建对象

$objWSSEToken = new clsWSSEToken($objSoapVarWSSEAuth);

步骤6:从Token类的对象

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

步骤7:为“安全”节点创建SoapVar

$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 Client的对象

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

步骤10:设置soapclient对象的标头

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

步骤11:最后调用方法

$objResponse = $objClient->__soapCall($strMethod, $requestPayloadString);

答案 4 :(得分:1)

我采用了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));

答案 5 :(得分:0)

$client = new SoapClient("some.wsdl", array('login'    => "some_name",
                                            'password' => "some_password"));

From the php documentation

答案 6 :(得分:0)

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