THTTPRIO未触发OnNeedClientCertificate事件,SOAP证书调用弹窗问题
解决Delphi THTTPRIO调用SOAP服务时自动选择指定证书的问题
问题分析
你绑定了OnNeedClientCertificate事件但未触发,通常有两个核心原因:
- 事件绑定时机错误:
THTTPRIO的HTTPWebNode.HTTP对象可能在调用GetrnoSoap后才完成初始化,提前绑定会导致事件关联失效。 - 服务器证书要求类型:部分服务器要求客户端在握手阶段预先提供证书,而非等待服务器触发证书请求,此时
OnNeedClientCertificate事件不会被调用。
解决方案
方案1:修正事件绑定时机
将事件绑定移到GetrnoSoap调用之后,确保HTTP对象已创建完成:
var RIO := THTTPRIO.Create(nil); try var svc := wsETurizemPorocanje.GetrnoSoap(false, '', RIO); // 此时HTTPWebNode的HTTP对象已初始化,再绑定事件 RIO.HTTPWebNode.HTTP.OnNeedClientCertificate := DoOnClientCertificateNeeded; RIO.HTTPWebNode.GetHTTPReqResp.PreemptiveAuthentication := true; data := TXmlData.Create; try data.LoadFromXml(xml.XML); var errors := svc.oddajPorocilo('UserName', 'Password', data , 1); finally data.Free; end; finally RIO.Free; end;
方案2:手动指定证书(更可靠)
直接从Windows证书存储中查找目标证书,添加到HTTP客户端的证书列表并设置自动选择,跳过事件触发逻辑:
1. 实现证书查找函数
添加Winapi.WinCrypt单元,编写根据序列号查找证书的函数(注意证书序列号在Windows存储中是字节倒序存储的):
uses Winapi.WinCrypt, System.SysUtils; function ReverseBytes(var Bytes: TBytes): TBytes; var I, J: Integer; Temp: Byte; begin Result := Bytes; J := Length(Result) - 1; for I := 0 to (Length(Result) div 2) - 1 do begin Temp := Result[I]; Result[I] := Result[J]; Result[J] := Temp; Dec(J); end; end; function FindCertificateBySerial(const SerialNumber: string): TCertificate; var CertStore: HCERTSTORE; Cert: PCCERT_CONTEXT; CleanSerial: string; SerialBytes: TBytes; I: Integer; begin Result := nil; CleanSerial := StringReplace(SerialNumber, ' ', '', [rfReplaceAll]); if Length(CleanSerial) mod 2 <> 0 then Exit; // 将十六进制序列号转为字节数组 SetLength(SerialBytes, Length(CleanSerial) div 2); for I := 0 to Length(CleanSerial) div 2 - 1 do SerialBytes[I] := StrToInt('$' + Copy(CleanSerial, I*2+1, 2)); // 反转字节(适配Windows存储的序列号格式) SerialBytes := ReverseBytes(SerialBytes); // 打开当前用户的"个人"证书存储;若证书在本地计算机存储,改用CERT_SYSTEM_STORE_LOCAL_MACHINE CertStore := CertOpenSystemStore(0, PChar('MY')); if CertStore = nil then Exit; try // 遍历存储中的所有证书 Cert := CertFindCertificateInStore(CertStore, X509_ASN_ENCODING or PKCS_7_ASN_ENCODING, 0, CERT_FIND_ANY, nil, nil); while Cert <> nil do begin // 匹配序列号 if (Cert^.pCertInfo^.SerialNumber.cbData = Length(SerialBytes)) and CompareMem(Cert^.pCertInfo^.SerialNumber.pbData, @SerialBytes[0], Length(SerialBytes)) then begin // 检查证书有效期 if (Cert^.pCertInfo^.NotBefore.dwLowDateTime <= GetTickCount64) and (Cert^.pCertInfo^.NotAfter.dwLowDateTime >= GetTickCount64) then begin Result := TCertificate.Create(Cert); Break; end else raise Exception.Create('目标证书已过期'); end; Cert := CertFindCertificateInStore(CertStore, X509_ASN_ENCODING or PKCS_7_ASN_ENCODING, 0, CERT_FIND_ANY, nil, Cert); end; finally CertCloseStore(CertStore, 0); end; end;
2. 在SOAP调用中使用证书
var RIO: THTTPRIO; svc: rnoSoap; TargetCert: TCertificate; data: TXmlData; errors: { 替换为实际返回类型 }; begin RIO := THTTPRIO.Create(nil); TargetCert := nil; data := nil; try // 查找指定序列号的证书 TargetCert := FindCertificateBySerial(AppSettings.Server.Hostel.ETurizemSettings.CertSerial); if not Assigned(TargetCert) then raise Exception.Create('未找到指定序列号的证书'); // 将证书添加到HTTP客户端并设置自动选择 RIO.HTTPWebNode.HTTP.ClientCertificates.Add(TargetCert); RIO.HTTPWebNode.HTTP.ClientCertificateIndex := 0; RIO.HTTPWebNode.GetHTTPReqResp.PreemptiveAuthentication := true; svc := wsETurizemPorocanje.GetrnoSoap(false, '', RIO); data := TXmlData.Create; data.LoadFromXml(xml.XML); errors := svc.oddajPorocilo('UserName', 'Password', data , 1); finally data.Free; TargetCert.Free; RIO.Free; end; end;
额外注意事项
- 证书存储权限:若证书在「本地计算机」存储,程序需以管理员权限运行才能访问。
- 序列号格式:确保传入的序列号是纯十六进制字符串,无多余空格或分隔符。
- 有效期验证:查找函数中已包含有效期检查,若证书过期会直接抛出异常。
内容的提问来源于stack exchange,提问作者Marko Milutinović
相关产品推荐
相关产品推荐

