PHP调用KBO SOAP API遭遇身份验证错误求助
我对该领域不太熟悉,正尝试用PHP调用KBO SOAP API,参考官方文档。前期流程正常,但身份验证环节始终失败,触发安全错误。
已编写PHP代码实现WSSE认证(生成nonce、timestamp、passwordDigest),并切换为SOAP_1_1版本,但仍报错:
[14-Feb-2023 17:09:24 UTC] PHP Fatal error: Uncaught SoapFault exception: [ns1:SecurityError] A security error was encountered when verifying the message in
Stack trace:
#0 /Users/mymac/Sites/cboxform/api-call/index.php(55): SoapClient->__soapCall('ReadEnterprise', Array)
#1 {main}
thrown in /Users/mymac/Sites/cboxform/api-call/index.php on line 55
需求是构造符合要求的SOAP请求:带包含摘要密码、有效期300秒的timestamp和nonce的认证头,请求需包含RequestContext结构,调用ReadEnterprise接口,目标端点为测试环境的KBO服务。
核心问题与解决方案
1. 修正密码摘要计算逻辑
之前的代码使用base64编码后的nonce字符串参与SHA1计算,这是错误的。正确做法是用原始随机字节来生成摘要,这是认证失败的核心原因。
2. 修正WSSE头传递方式
WSSE安全头是SOAP Envelope的一部分,不是HTTP请求头,必须用SoapHeader类构造并添加到SOAP客户端,而非放在Authorization HTTP头里。
3. 补充必填的RequestContext结构
根据KBO API要求,所有请求必须包含RequestContext,至少指定语言信息。
修正后的完整代码
<?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'; // 生成WSSE认证参数 $timestamp = gmdate('Y-m-d\TH:i:s\Z'); $nonce_bytes = random_bytes(16); // 原始随机字节 $nonce = base64_encode($nonce_bytes); // 用原始字节计算密码摘要 $passwordDigest = base64_encode(sha1($nonce_bytes . $timestamp . $password, true)); // 构造WSSE SOAP头 $wsseNamespace = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'; $wsuNamespace = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'; $usernameToken = new stdClass(); $usernameToken->Username = new SoapVar($username, XSD_STRING, null, $wsseNamespace, null, $wsseNamespace); // 构造带Type属性的Password节点 $passwordNode = new SoapVar($passwordDigest, XSD_STRING, null, $wsseNamespace, null, $wsseNamespace); $usernameToken->Password = new SoapVar($passwordNode, SOAP_ENC_OBJECT, null, $wsseNamespace, 'Password', $wsseNamespace); $usernameToken->Password->attributes = array('Type' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest'); $usernameToken->Nonce = new SoapVar($nonce, XSD_STRING, null, $wsseNamespace, null, $wsseNamespace); $usernameToken->Created = new SoapVar($timestamp, XSD_STRING, null, $wsuNamespace, null, $wsuNamespace); $securityHeader = new SoapVar($usernameToken, SOAP_ENC_OBJECT, null, $wsseNamespace, 'UsernameToken', $wsseNamespace); $soapHeader = new SoapHeader($wsseNamespace, 'Security', $securityHeader, true); // SOAP客户端配置 $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( 'ssl' => array( 'verify_peer' => true, // 生产环境必须开启 'verify_peer_name' => true ) )) ); $client = new SoapClient($wsdl, $options); $client->__setSoapHeaders($soapHeader); // 添加WSSE头 // 构造包含RequestContext的请求参数 $enterpriseNumber = '0810.002.854'; $request = array( 'RequestContext' => array( 'Language' => 'EN', // 可选FR/NL/EN,按需求设置 'ClientIPAddress' => $_SERVER['REMOTE_ADDR'] // 可选,API要求的客户端IP ), 'EnterpriseNumber' => $enterpriseNumber ); try { $response = $client->__soapCall('ReadEnterprise', array($request)); // 调试用:打印请求和响应XML echo "Request XML:\n" . htmlspecialchars($client->__getLastRequest()) . "\n"; echo "Response XML:\n" . htmlspecialchars($client->__getLastResponse()) . "\n"; var_dump($response); } catch (SoapFault $e) { echo "Error: " . $e->getMessage() . "\n"; echo "Request XML:\n" . htmlspecialchars($client->__getLastRequest()) . "\n"; } ?>
额外说明
- timestamp有效期:代码中生成的UTC时间戳符合300秒有效期要求,确保请求在生成时间5分钟内发送。
- SSL验证:测试环境可临时关闭
verify_peer和verify_peer_name,但生产环境必须开启,避免安全风险。 - 调试技巧:通过
__getLastRequest()和__getLastResponse()查看完整的SOAP报文,方便对比官方文档排查格式问题。
内容的提问来源于stack exchange,提问作者Porcellino80

