如何在 WSDL 中添加 WS-Security Password Digest
结论
WS-Security Password Digest 不应写在 WSDL 的 <operation> 中,也不能把用户名、密码等认证字段放入 SOAP Body。WSDL 用于描述服务接口,实际的 UsernameToken 需要由调用方在每次请求时动态生成,并放入 SOAP Header,服务端再对其进行校验。
SOAP UI 返回 procedure not found,通常表示请求没有匹配到服务端操作。常见原因包括 endpoint、SOAPAction、操作名称或命名空间不正确,一般与 Password Digest 校验失败无关。
Password Digest 的计算方式
符合 WS-Security UsernameToken Profile 的摘要计算公式如下:
Password_Digest = Base64(SHA-1(nonce_bytes + created_utf8 + password_utf8))
其中:
nonce_bytes是随机数的原始字节,不是 Base64 编码后的字符串。created_utf8是<wsu:Created>中时间戳的 UTF-8 字节。password_utf8是明文密码的 UTF-8 字节。<wsse:Nonce>保存的是Base64(nonce_bytes)。- 每次请求都要生成新的
Nonce和Created。
不能使用 SHA1(username + password) 代替上述计算,也不能把明文密码直接放入 Digest 类型的 <wsse:Password>。
正确的 SOAP Header
下面是 SOAP 1.1 请求的基本结构。Body 中的操作名和命名空间需要替换为 WSDL 实际定义的值:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
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"
xmlns:mer="http://example.com/merchant">
<soapenv:Header>
<wsse:Security soapenv:mustUnderstand="1">
<wsse:UsernameToken>
<wsse:Username>XXXX</wsse:Username>
<wsse:Password
Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">BASE64_SHA1_DIGEST</wsse:Password>
<wsse:Nonce
EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">BASE64_NONCE</wsse:Nonce>
<wsu:Created>2026-09-02T08:30:00Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<mer:OperationName>
<!-- operation parameters -->
</mer:OperationName>
</soapenv:Body>
</soapenv:Envelope>
命名空间 URI、大小写和 Type 属性都必须准确。不要根据 PHP 方法名或页面示例猜测 OperationName,应以 WSDL 中的 <wsdl:operation> 及其绑定为准。
在 SOAP UI 中配置
SOAP UI 可以自动计算 Digest,不需要手工处理:
- 打开项目的 WS-Security Configurations。
- 在 Outgoing WS-Security Configurations 中新增配置。
- 添加 Username 项。
- 填写商户 username 和 MS-WSP password。
- 将 Password Type 设为
PasswordDigest。 - 启用
Add Nonce和Add Created。 - 在具体请求的 Auth/WS-Security 设置中选择该 Outgoing 配置。
- 检查生成的 XML,确认
<wsse:Security>位于<soapenv:Header>下。
如果调用方向是 Paga 调用 Joomla 提供的 merchant web-service,而非 Joomla 主动调用 Paga,处理方式正好相反。Joomla 不需要生成这个 Header,而要从收到的请求中读取并验证 UsernameToken。
Joomla/PHP 调用端示例
PHP 原生 SoapClient 没有完整的 WS-Security UsernameToken API。可以动态构造 Header,但每次请求都必须重新生成摘要:
<?php
function createWsSecurityHeader(
SoapClient $client,
string $username,
string $password
): SoapHeader {
$nonce = random_bytes(16);
$created = gmdate('Y-m-d\TH:i:s\Z');
$digest = base64_encode(
sha1($nonce . $created . $password, true)
);
$nonceBase64 = base64_encode($nonce);
$xml = sprintf(
'<wsse:Security
xmlns:wsse="%s"
xmlns:wsu="%s">
<wsse:UsernameToken>
<wsse:Username>%s</wsse:Username>
<wsse:Password Type="%s">%s</wsse:Password>
<wsse:Nonce EncodingType="%s">%s</wsse:Nonce>
<wsu:Created>%s</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>',
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
htmlspecialchars($username, ENT_XML1 | ENT_QUOTES, 'UTF-8'),
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest',
$digest,
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary',
$nonceBase64,
$created
);
$security = new SoapVar($xml, XSD_ANYXML);
return new SoapHeader(
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
'Security',
$security,
true
);
}
$client = new SoapClient('https://example.com/service?wsdl', [
'trace' => true,
'exceptions' => true,
'cache_wsdl' => WSDL_CACHE_NONE,
]);
$client->__setSoapHeaders(
createWsSecurityHeader($client, 'XXXX', 'YYYY')
);
$result = $client->__soapCall('OperationName', [
[
// operation parameters
],
]);
部分 PHP/SOAP 环境会为 SoapVar 包装的 XML 添加额外节点,或者改变 Header 结构。正式接入前,需要通过 $client->__getLastRequest() 检查最终发送的 XML,不能只查看生成 Header 的 PHP 代码。
WSDL 中是否需要声明安全策略
如果对方要求 WSDL 明确描述安全能力,可以添加 WS-Policy/WS-SecurityPolicy。不过,这类策略只是服务能力声明,既不会自动完成认证,也不能代替运行时校验。
例如,可以在概念上声明 UsernameToken:
<wsp:Policy
xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
xmlns:sp="http://schemas.xmlsoap.org/ws/2005/07/securitypolicy">
<sp:SupportingTokens>
<wsp:Policy>
<sp:UsernameToken>
<wsp:Policy>
<sp:WssUsernameToken10/>
</wsp:Policy>
</sp:UsernameToken>
</wsp:Policy>
</sp:SupportingTokens>
</wsp:Policy>
WS-Policy 和 WS-SecurityPolicy 有多个版本,不同 SOAP 框架之间的兼容性也有较大差异。除非 Paga 明确指定 Policy 的命名空间和版本,否则不要仅根据示例把这段声明加入生产 WSDL。大多数互操作场景只要求实际发送的 SOAP Header 符合规范。
排查 procedure not found
可以先暂时排除 WS-Security,逐项检查请求路由:
- 请求 URL 是否为 WSDL 中
<soap:address location="...">指定的 endpoint。 - Body 第一层元素是否与对应 operation 的输入消息一致。
- operation 名称的大小写是否完全正确。
- Body 元素使用的命名空间是否正确。
- 是否混用了 SOAP 1.1 和 SOAP 1.2。
- HTTP
SOAPAction是否与 WSDL 中的<soap:operation soapAction="...">一致。 - Joomla 路由是否将 SOAP POST 请求交给了正确的组件或入口文件。
- 服务端是否已经注册并公开该 operation。
SOAP 1.1 通常使用:
Content-Type: text/xml; charset=utf-8
SOAPAction: "WSDL 中声明的 soapAction"
SOAP 1.2 通常使用:
Content-Type: application/soap+xml; charset=utf-8; action="..."
如果服务端已经识别 operation,但认证失败,通常会返回 FailedAuthentication、InvalidSecurity、时间戳过期或类似的 SOAP Fault,而不是 procedure not found。
服务端校验时的注意事项
Joomla 作为接收端时,除了重新计算 Digest,还要处理以下事项:
- 根据商户 username 查找对应的原始密码。
- 使用常量时间比较摘要结果,例如
hash_equals()。 - 限制
Created的有效时间窗口,同时考虑服务器时钟偏差。 - 保存近期使用过的 Nonce,并拒绝重复请求,以防止重放攻击。
- 拒绝缺少
Nonce、Created或 Digest 类型不正确的请求。 - 全程使用 HTTPS。Password Digest 不能代替 TLS。
- 不要记录明文密码、完整安全 Header 或其他可重复利用的认证数据。
处理顺序上,应先修正 endpoint、operation、命名空间和 SOAPAction,让服务端能够识别调用。之后再通过 SOAP UI 的 Outgoing WS-Security 配置加入 UsernameToken。只修改 WSDL,不能让 Password Digest 自动生效。
备注:内容仅供参考。
