如何用Delphi的TIdHTTP实现带UsernameToken的SOAP服务访问?
我来帮你搞定这个问题!从你提供的服务器配置来看,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

