我有点脱离了自己的舒适区。我正在尝试使用 PHP 创建 SOAP 请求,并且我正在使用著名的 KBO API,这是文档:
所以基本上一切都很顺利,但我仍然在与身份验证有关的问题上苦苦挣扎,这使我的整个小框架失败了:
<?php
$endpoint = 'https://kbopub-acc.economie.fgov.be/kbopubws110000/services/wsKBOPub';
$wsdl = 'https://kbopub-acc.economie.fgov.be/kbopubws110000/services/wsKBOPub?wsdl';
$username = 'myusername';
$password = 'mypassword';
$timestamp = gmdate('Y-m-d\TH:i:s\Z');
$nonce = base64_encode(random_bytes(16));
$passwordDigest = base64_encode(sha1($nonce . $timestamp . $password, true));
$header = '
<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>' . $username . '</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' . $passwordDigest . '</wsse:Password>
<wsse:Nonce>' . $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>';
$options = array(
'soap_version' => SOAP_1_1,
'trace' => true,
'exceptions' => true,
'encoding' => 'UTF-8',
'cache_wsdl' => WSDL_CACHE_NONE,
'stream_context' => stream_context_create(array(
'http' => array(
'header' => 'Authorization: WSSE profile=' . $header,
'user_agent' => 'PHPSoapClient'
),
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false
)
))
);
$context = stream_context_create(array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
));
$options['stream_context'] = $context;
// $options = array(
// 'soap_version' => SOAP_1_1
// );
$client = new SoapClient($wsdl, $options);
// Set up the request parameters
$enterpriseNumber = '0810.002.854';
$request = array('EnterpriseNumber' => $enterpriseNumber);
// Call the SOAP operation with the request parameters
$response = $client->__soapCall('ReadEnterprise', array($request));
// Get the request and response XML
$requestXML = $client->__getLastRequest();
$responseXML = $client->__getLastResponse();
var_dump($response);
我还遵循了 PHP 日志文件中的一些建议,其中编写了使用不同版本的肥皂版本:SOAP_1_1 而不是 SOAP_1_2。
我发现真正具有挑战性的是我无法以任何方式调试它;我不知道如何检查错误消息,或者我的错误日志文件中没有任何提示,它只是提到了安全检查,但没有太多。
有人遇到同样的问题吗?
顺便说一句,我遇到的错误如下:
[14-Feb-2023 17:09:24 UTC] PHP 致命错误:未捕获的 SoapFault 异常:[ns1:SecurityError] 验证消息时遇到安全错误 堆栈跟踪: #0 /Users/mymac/Sites/cboxform/api-call/index.php(55): SoapClient->__soapCall('ReadEnterprise', Array) #1 {主要} 扔在 /Users/mymac/Sites/cboxform/api-call/index.php 第 55 行
TLTR:
总而言之,我需要一个如下所示的请求:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mes="http://economie.fgov.be/kbopub/webservices/v1/messages" xmlns:dat="http://economie.fgov.be/kbopub/webservices/v1/datamodel">
<soapenv:Header>
<mes:RequestContext>
<mes:Id>myid</mes:Id>
<mes:Language>fr</mes:Language>
</mes:RequestContext>
</soapenv:Header>
<soapenv:Body>
<mes:ReadEnterpriseRequest>
<dat:EnterpriseNumber>0206231995</dat:EnterpriseNumber>
</mes:ReadEnterpriseRequest>
</soapenv:Body>
</soapenv:Envelope>
带有摘要密码的标头,以及将在 300 秒后过期到此端点的时间戳和随机数:
https://kbopub-acc.economie.fgov.be/kbopubws110000/services/wsKBOPub?wsdl
还有用户名。
您用来验证 SOAP 请求的安全令牌似乎存在问题。 请检查
timezone
格式为 UTC (YYYY-MM-DDTHH:MM:SSZ)
,Nonce
应是为每个请求生成的随机值,并且 PasswordDigest
值计算正确,PasswordDigest
(SHA-1 哈希)和 的串联值Nonce
,以二进制格式创建 timestamp
、password
。
您也可以尝试打印出
timestamp
、nonce
和passwordDigest
的值,以便于调试
您可以尝试替换以下代码中的第 20 行吗:
$options = array(
'soap_version' => SOAP_1_1,
'trace' => true,
'exceptions' => true,
'encoding' => 'UTF-8',
'cache_wsdl' => WSDL_CACHE_NONE,
'stream_context' => stream_context_create(array(
'http' => array(
'header' => 'Authorization: WSSE ' . $header,
'user_agent' => 'PHPSoapClient'
),
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false
)
))
);
$context = stream_context_create(array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
));
WSSE 标头在其后的空格内使用正确的配置文件值
KBO 公共 Web 服务使用
ns1:SecurityError
作为捕获所有异常,这使得很难确定实际原因。
作为第一步,我发现它有效:
这通常在设置语言代码和企业编号后起作用。
但是实际有效负载是多少?
log.info mockRequest.requestContent
。请注意,密码摘要只能使用一次。在后续使用时,它还将返回包罗万象的 ns1:SecurityError。工作 SQL 实现的示例如下(如 https://forums.invantive.com/t/uitlezen-kbo-public-search-api-geeft-foutmelding-ns1-securityerror/3424/2 所示):
--
-- Tested according to http://www.herongyang.com/Web-Services/WS-Security-Validate-Password-Digest-String.html
-- using:
-- l_created := '2014-06-21T12:43:21.791Z';
-- l_password := 'iLoveDogs';
-- l_username := 'herong';
--
declare
l_nonce blob;
l_created varchar2;
l_expires varchar2;
l_wsse_created varchar2;
l_password_digest varchar2;
l_xml varchar2;
l_out_contents_char varchar2;
l_out_http_status_code int32;
l_created_date datetime;
l_expires_date datetime;
--
l_enterprise_number varchar2 := '0670979187';
l_username varchar2 := 'wsot9999';
l_password varchar2 := 'secret';
l_language_code varchar2 := 'nl';
l_msg_id varchar2 := to_char(newid());
begin
l_created_date := sysdateutc;
--
-- Default maximum offset of wss4j is 300 seconds.
--
l_expires_date := l_created_date + 300/86400;
--
l_created := to_char(l_created_date, 'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"');
l_expires := to_char(l_expires_date, 'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"');
l_wsse_created := to_char(l_created_date, 'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"');
--
l_nonce:=random_blob(24);
--
-- Calculate digested password.
--
l_password_digest := base64_encode(hex_to_blob(sha1(l_nonce || to_binary(l_wsse_created) || to_binary(l_password))));
--
l_xml := '<soapenv:Envelope xmlns:dat="http://economie.fgov.be/kbopub/webservices/v1/datamodel" xmlns:mes="http://economie.fgov.be/kbopub/webservices/v1/messages" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">';
l_xml := l_xml || chr(13) || chr(10) || ' <soapenv:Header>';
l_xml := l_xml || chr(13) || chr(10) || ' <wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">';
l_xml := l_xml || chr(13) || chr(10) || ' <wsu:Timestamp wsu:Id="TS-A4DC6AF47AFDBFB025168519485171276">';
l_xml := l_xml || chr(13) || chr(10) || ' <wsu:Created>' || l_created || '</wsu:Created>';
l_xml := l_xml || chr(13) || chr(10) || ' <wsu:Expires>' || l_expires || '</wsu:Expires>';
l_xml := l_xml || chr(13) || chr(10) || ' </wsu:Timestamp>';
l_xml := l_xml || chr(13) || chr(10) || ' <wsse:UsernameToken wsu:Id="UsernameToken-A4DC6AF47AFDBFB025168519485171275">';
l_xml := l_xml || chr(13) || chr(10) || ' <wsse:Username>' || xmlencode(l_username) || '</wsse:Username>';
l_xml := l_xml || chr(13) || chr(10) || ' <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' || xmlencode(l_password_digest) || '</wsse:Password>';
l_xml := l_xml || chr(13) || chr(10) || ' <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' || xmlencode(l_nonce) || '</wsse:Nonce>';
l_xml := l_xml || chr(13) || chr(10) || ' <wsu:Created>' || l_wsse_created || '</wsu:Created>';
l_xml := l_xml || chr(13) || chr(10) || ' </wsse:UsernameToken>';
l_xml := l_xml || chr(13) || chr(10) || ' </wsse:Security>';
l_xml := l_xml || chr(13) || chr(10) || ' <mes:RequestContext>';
l_xml := l_xml || chr(13) || chr(10) || ' <mes:Id>' || xmlencode(l_msg_id) || '</mes:Id>';
l_xml := l_xml || chr(13) || chr(10) || ' <mes:Language>' || xmlencode(l_language_code) || '</mes:Language>';
l_xml := l_xml || chr(13) || chr(10) || ' </mes:RequestContext>';
l_xml := l_xml || chr(13) || chr(10) || ' </soapenv:Header>';
l_xml := l_xml || chr(13) || chr(10) || ' <soapenv:Body>';
l_xml := l_xml || chr(13) || chr(10) || ' <mes:ReadEnterpriseRequest>';
l_xml := l_xml || chr(13) || chr(10) || ' <dat:EnterpriseNumber>' || xmlencode(l_enterprise_number) || '</dat:EnterpriseNumber>';
l_xml := l_xml || chr(13) || chr(10) || ' </mes:ReadEnterpriseRequest>';
l_xml := l_xml || chr(13) || chr(10) || ' </soapenv:Body>';
l_xml := l_xml || chr(13) || chr(10) || '</soapenv:Envelope>';
--
select htp.CONTENTS_CHAR
, htp.http_status_code
into l_out_contents_char
, l_out_http_status_code
from HTTPDownload@DataDictionary
( url => 'https://kbopub-acc.economie.fgov.be/kbopubws110000/services/wsKBOPub'
, acceptMimeType => '*/*'
, contentType => 'text/xml;charset=UTF-8'
, method => 'POST'
, textPayload => l_xml
, ignoreWebError => false
) htp
;
dbms_output.put_line(l_out_contents_char);
end;
我在使用食谱连接到https://kbopub-acc.economie.fgov.be/kbopubws110000/services/wsKBOPub时遇到了同样的错误:
PHP 致命错误:未捕获的 SoapFault 异常:[ns1:SecurityError] 验证 /var/www/html/soap.php:39 中的消息时遇到安全错误
不知道出了什么问题。
<?php
$wsdl = 'https://kbopub-acc.economie.fgov.be/kbopubws110000/services/wsKBOPub?wsdl';
$username = 'user';
$password = 'pass';
$client = new WSSoapClient($wsdl);
$client->setUsernameToken($username, $password);
$response = $client->__soapCall('ReadEnterprise', ['EnterpriseNumber' => '0810002854']);
var_dump($response);
class WSSoapClient extends \SoapClient
{
private $nonce;
private $timestamp;
private $timestampExpires;
private $username;
private $password;
public function setUsernameToken($username, $password)
{
$this->username = $username;
$this->password = $password;
}
public function __soapCall(
$function_name,
$arguments,
$options = null,
$input_headers = null,
&$output_headers = null
) {
$headers = [$this->generateWSSecurityHeader(), $this->generateRequestContextHeader()];
return parent::__soapCall(
$function_name,
$arguments,
$options,
$headers
);
}
private function generatePasswordDigest()
{
$this->nonce = mt_rand();
$currentTime = time();
$this->timestamp = gmdate("Y-m-d\TH:i:s", $currentTime) . 'Z';
$this->timestampExpires = gmdate("Y-m-d\TH:i:s", $currentTime + 300) . 'Z';
return base64_encode(sha1($this->nonce . $this->timestamp . $this->password));
}
private function generateWSSecurityHeader()
{
$passwordDigest = $this->generatePasswordDigest();
$xml = '<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsu:Timestamp xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsu:Created>' . $this->timestamp . '</wsu:Created>
<wsu:Expires>' . $this->timestampExpires . '</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken>
<wsse:Username>' . $this->username . '</wsse:Username>
<wsse:Password>' . $passwordDigest . '</wsse:Password>
<wsse:Nonce>' . base64_encode($this->nonce) . '</wsse:Nonce>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' . $this->timestamp . '</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>';
return new \SoapHeader(
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
'Security',
new \SoapVar($xml, XSD_ANYXML),
true
);
}
private function generateRequestContextHeader()
{
$xml = '<mes:RequestContext xmlns:mes="http://economie.fgov.be/KBOpub/webservices/v1/messages">
<mes:Id>' . $this->getUuid() . '</mes:Id>
<mes:Language>fr</mes:Language>
</mes:RequestContext>';
return new SoapHeader(
'http://economie.fgov.be/KBOpub/webservices/v1/messages',
'RequestContext',
new SoapVar($xml, XSD_ANYXML),
true
);
}
private function getUuid()
{
$uuid = [
'time_low' => 0,
'time_mid' => 0,
'time_hi' => 0,
'clock_seq_hi' => 0,
'clock_seq_low' => 0,
'node' => []
];
$uuid['time_low'] = mt_rand(0, 0xffff) + (mt_rand(0, 0xffff) << 16);
$uuid['time_mid'] = mt_rand(0, 0xffff);
$uuid['time_hi'] = (4 << 12) | (mt_rand(0, 0x1000));
$uuid['clock_seq_hi'] = (1 << 7) | (mt_rand(0, 128));
$uuid['clock_seq_low'] = mt_rand(0, 255);
for ($i = 0; $i < 6; $i++) {
$uuid['node'][$i] = mt_rand(0, 255);
}
$uuid = sprintf(
'%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x',
$uuid['time_low'],
$uuid['time_mid'],
$uuid['time_hi'],
$uuid['clock_seq_hi'],
$uuid['clock_seq_low'],
$uuid['node'][0],
$uuid['node'][1],
$uuid['node'][2],
$uuid['node'][3],
$uuid['node'][4],
$uuid['node'][5]
);
return $uuid;
}
}