使用PHP连接到受WS-Security保护的Web服务 [英] Connecting to WS-Security protected Web Service with PHP

查看:67
本文介绍了使用PHP连接到受WS-Security保护的Web服务的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试连接到受密码保护且URL为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

如果我尝试将服务定义为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"添加到数组,但这不好.问题在于,我什至不知道我是否要到达远程站点,更不用说它是否拒绝了请求.

解决方案

问题似乎是WSDL文档受到了某种保护(基本身份验证-我不认为SoapClient支持摘要身份验证,所以您(在这种情况下会很不幸),因此SoapClient无法读取和解析服务描述.

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

  1. SoapClient构造函数调用上将登录凭据添加到WSDL URL

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

    这应该是最简单的解决方案-但在PHP中错误#27777 据记载,这也不起作用(我没有尝试过).

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

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

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

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

在本地拥有WSDL文件是第一步-这将使SoapClient知道如何与服务通信.可以直接从服务位置,从另一台服务器提供服务或从本地文件中读取WSDL,因为服务URL是在WSDL中编码的,因此SoapClient始终知道在哪里寻找服务端点.

第二个问题是SoapClient不支持 WS-Security 规范本身,这意味着您必须扩展SoapClient才能处理特定的标头.添加所需行为的扩展点是 SoapClient::__doRequest() -在将XML有效负载发送到服务端点之前对其进行处理.但是我认为,自己实施WS-Security解决方案将需要对特定WS-Security规范有相当的了解.也许还可以使用 和适当的 SoapHeader s,但我对此表示怀疑这将起作用,将自定义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模块,则应该查看

and then go to the site on the browser, I get:

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

If I try defining the service as a Soap Server, like:

$server= new SoapServer("https://example.com/WSDL/nameofservice");

I get:

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

I haven't tried sending a raw request envelope yet to see what the server returns, but that may be a workaround. But I was hoping someone could tell me how I can set it up using the php built-in classes. I tried adding "userName" and "password" to the array, but that was no good. The problem is that I can't even tell if I'm reaching the remote site at all, let alone whether it is refusing the request.

The problem seems to be that the WSDL document is somehow protected (basic authentication - I don't thinkg that digest authentication is supported with SoapClient, so you'd be out of luck in this case) and that the SoapClient therefore cannot read and parse the service description.

First of all you should try to open the WSDL location in your browser to check if you're presented an authentication dialog. If there is an authentication dialog you must make sure that the SoapClient uses the required login credentials on retrieving the WSDL document. The problem is that SoapClient will only send the credentials given with the login and password options (as well as the local_cert option when using certificate authentication) on creating the client when invoking the service, not when fetching the WSDL (see here). There are two methods to overcome this problem:

  1. Add the login credentials to the WSDL url on the SoapClient constructor call

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

    This should be the most simple solution - but in PHP Bug #27777 it is written that this won't work either (I haven't tried that).

  2. Fetch the WSDL manually using the HTTP stream wrapper or ext/curl or manually through your browser or via wgetfor example, store it on disk and instantiate the SoapClient with a reference to the local WSDL.

    This solution can be problematic if the WSDL document changes as you have to detect the change and store the new version on disk.

If no authentication dialog is shown and if you can read the WSDL in your browser, you should provide some more details to check for other possible errors/problems.

This problem is definitively not related to the service itself as SoapClient chokes already on reading the service descripion document before issuing a call to the service itself.

EDIT:

Having the WSDL file locally is a first step - this will allow the SoapClient to know how to communicate with the service. It doesn't matter if the WSDL is directly served from the service location, from another server or is read from a local file - service urls are coded within the WSDL so SoapClient always knows where to look for the service endpoint.

The second problem now is that SoapClient has no support for the WS-Security specifications natively, which means you must extend SoapClient to handle the specific headers. An extension point to add the required behaviour would be SoapClient::__doRequest() which pre-processes the XML payload before sending it to the service endpoint. But I think that implementing the WS-Security solution yourself will require a decent knowledge of the specific WS-Security specifications. Perhaps WS-Security headers can also be created and packed into the XML request by using SoapClient::__setSoapHeaders() and the appropriate SoapHeaders but I doubt that this will work, leaving the custom SoapClient extension as the lone possibility.

A simple SoapClient extension would be

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

For a basic WS-Security authentication you would have to add the following to the SOAP-header:

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

But as I said above: I think that much more knowledge about the WS-Security specification and the given service architecture is needed to get this working.

If you need an enterprise grade solution for the whole WS-* specification range and if you can install PHP modules you should have a look at the WSO2 Web Services Framework for PHP (WSO2 WSF/PHP)

这篇关于使用PHP连接到受WS-Security保护的Web服务的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆