You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Delphi的TIdHTTP实现带UsernameToken的SOAP服务访问?

解决Delphi Indy TIdHTTP添加WS-Security UsernameToken的问题

我来帮你搞定这个问题!从你提供的服务器配置来看,TransportWithMessageCredential意味着两点:一是必须通过HTTPS访问站点,二是要在SOAP请求的SOAP Header里带上符合WS-Security标准的UsernameToken(你提到的urladdressforoasis应该是指OASIS规范中PasswordDigest类型对应的URI:http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest)。

下面是一步步的实现方案:

1. 先给TIdHTTP配置HTTPS支持

因为服务器要求TransportWithMessageCredential,所以必须启用SSL/TLS。你需要给TIdHTTP绑定TIdSSLIOHandlerSocketOpenSSL组件,代码示例如下:

var
  IdHTTP: TIdHTTP;
  IdSSLIOHandler: TIdSSLIOHandlerSocketOpenSSL;
begin
  IdHTTP := TIdHTTP.Create(nil);
  try
    IdSSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
    try
      // 这里根据服务器支持的SSL版本调整,一般用TLS1.2就没问题
      IdSSLIOHandler.SSLOptions.Method := sslvTLSv1_2;
      IdSSLIOHandler.SSLOptions.Mode := sslmClient;
      IdHTTP.IOHandler := IdSSLIOHandler;
      // 后续步骤在这里执行
    finally
      IdSSLIOHandler.Free;
    end;
  finally
    IdHTTP.Free;
  end;
end;

⚠️ 注意:要确保你的程序运行目录下有OpenSSL的库文件(32位用libeay32.dll和ssleay32.dll,64位用libcrypto-1_1-x64.dll和libssl-1_1-x64.dll),不然SSL组件会报错。

2. 生成符合WS-Security标准的UsernameToken

WS-Security的UsernameToken不是普通的HTTP头,而是要嵌入到SOAP Envelope的Header节点里的XML片段,而且PasswordDigest类型需要结合随机Nonce、UTC时间戳和密码计算哈希。我写了个工具函数帮你生成这个片段:

function GenerateUsernameToken(const AUsername, APassword: string): string;
var
  LNonce: TBytes;
  LNonceBase64: string;
  LCreated: string;
  LPasswordDigest: TBytes;
  LPasswordDigestBase64: string;
  LHash: TIdHashSHA1;
begin
  // 生成随机Nonce(16字节随机数)
  SetLength(LNonce, 16);
  Randomize;
  CryptGenRandom(0, Length(LNonce), @LNonce[0]);
  LNonceBase64 := TIdEncoderMIME.EncodeBytes(LNonce);

  // 获取UTC时间,格式必须是ISO 8601(比如2024-05-20T12:34:56Z)
  LCreated := FormatDateTime('yyyy-mm-dd"T"hh:nn:ss"Z"', NowUTC);

  // 计算PasswordDigest:SHA1(Nonce + Created + 原始密码),然后Base64编码
  LHash := TIdHashSHA1.Create;
  try
    LPasswordDigest := LHash.HashBytesRaw(LNonce + TEncoding.UTF8.GetBytes(LCreated + APassword));
    LPasswordDigestBase64 := TIdEncoderMIME.EncodeBytes(LPasswordDigest);
  finally
    LHash.Free;
  end;

  // 构造UsernameToken的XML(注意命名空间不能错)
  Result := '<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">' +
            '<wsse:UsernameToken>' +
            '<wsse:Username>' + AUsername + '</wsse:Username>' +
            '<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">' + LPasswordDigestBase64 + '</wsse:Password>' +
            '<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">' + LNonceBase64 + '</wsse:Nonce>' +
            '<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' + LCreated + '</wsu:Created>' +
            '</wsse:UsernameToken>' +
            '</wsse:Security>';
end;

如果服务器确实要求urladdressforoasis作为Password的Type值,直接替换上面代码里的Type属性值就行。

3. 构造完整SOAP请求并发送

现在把生成的UsernameToken嵌入到SOAP Envelope的Header里,然后用TIdHTTP发送Post请求:

procedure SendSecuredSOAPRequest(const AURL, AUsername, APassword, ASOAPBody: string);
var
  IdHTTP: TIdHTTP;
  IdSSLIOHandler: TIdSSLIOHandlerSocketOpenSSL;
  LSOAPEnvelope: string;
  LRequestStream: TStringStream;
begin
  IdHTTP := TIdHTTP.Create(nil);
  try
    IdSSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
    try
      IdSSLIOHandler.SSLOptions.Method := sslvTLSv1_2;
      IdSSLIOHandler.SSLOptions.Mode := sslmClient;
      IdHTTP.IOHandler := IdSSLIOHandler;

      // 设置HTTP请求头,ContentType必须是text/xml,编码用UTF-8
      IdHTTP.Request.ContentType := 'text/xml; charset=utf-8';
      IdHTTP.Request.Accept := 'text/xml';
      // 记得加SOAPAction头,值要和你的服务操作的命名空间匹配
      IdHTTP.Request.CustomHeaders.Add('SOAPAction: "http://your-service-namespace/your-operation-name"');

      // 构造完整的SOAP Envelope
      LSOAPEnvelope := '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">' +
                       '<soapenv:Header>' +
                       GenerateUsernameToken(AUsername, APassword) +
                       '</soapenv:Header>' +
                       '<soapenv:Body>' +
                       ASOAPBody +
                       '</soapenv:Body>' +
                       '</soapenv:Envelope>';

      // 发送请求
      LRequestStream := TStringStream.Create(LSOAPEnvelope, TEncoding.UTF8);
      try
        // 这里可以把返回结果打印或者处理,我用ShowMessage示例
        ShowMessage(IdHTTP.Post(AURL, LRequestStream));
      finally
        LRequestStream.Free;
      end;
    finally
      IdSSLIOHandler.Free;
    end;
  finally
    IdHTTP.Free;
  end;
end;

几个关键坑要注意

  • 别把UsernameToken当成HTTP请求头加!WS-Security是SOAP级别的安全头,必须嵌入到SOAP Envelope的Header里,这是很多新手容易犯的错。
  • Nonce必须每次请求都生成新的随机值,Created必须是UTC时间,不然服务器的密码验证会失败。
  • SOAPAction头的值必须和你的服务提供的操作命名空间完全一致,不然服务器可能返回400错误。

内容的提问来源于stack exchange,提问作者RDT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:15:53