PAYATHON 2026

如何在 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,不需要手工处理:

  1. 打开项目的 WS-Security Configurations。
  2. 在 Outgoing WS-Security Configurations 中新增配置。
  3. 添加 Username 项。
  4. 填写商户 username 和 MS-WSP password。
  5. 将 Password Type 设为 PasswordDigest。
  6. 启用 Add Nonce 和 Add Created。
  7. 在具体请求的 Auth/WS-Security 设置中选择该 Outgoing 配置。
  8. 检查生成的 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 自动生效。

备注:内容仅供参考。