Vorweg die Bitte an einen Moderator: kann einer von euch Mods das Thema von "REST" nach "
SOAP" umbenennen, damit es auch von anderen korrekt gefunden wird? Ich hatte damals beim Erstellen offenbar einen kleinen Aussetzer.
Ich habe die Lösung für das Problem. Leider ist es auch in Delphi Rio 10.3.2 noch immer ein Bug in der
Unit System.Net.HttpClient.Win, den man nur gelöst bekommt, indem man eine eigene Version der
Unit in sein Projekt einbaut und die entscheidende Stelle patcht. Wer bessere Lösungen hat, gerne melden.
Ich versuche nochmal, das Problem, den Fehler und eine mögliche Lösung kurz zusammenzufassen (dann braucht ihr die bisherigen Beiträge nicht unbedingt lesen):
Problem:
Es soll ein Web Service innerhalb eines Unternehmsnetzwerks genutzt werden. Der Web Service wird auf einem Windows Server gehostet (konkret geht es hier um Web Services von Microsoft Dynamics NAV) und verlangt eine Authentifizierung, damit er genutzt werden kann. Da das ganze innerhalb eines Unternehmensnetzwerk passiert, kann eigentlich NTLM/Kerberos für die Authentifizierung genutzt und dabei die Credentials das aktuellen Benutzers genutzt werden. Heißt vereinfacht: Windows kümmert sich im Hntergrund um die Anmeldung am Web Service und der Benutzer muss seine Login-Daten nicht erneut eingeben bzw. man muss sie nicht selber irgendwo speichern. Bis Delphi 10.2.3 funktionierte das meist problemlos, ab Delphi 10.3.1 hat Emba die ganzen
SOAP-Bibliotheken technisch komplett umgebaut, um sie auf THTTPClient umzubauen. Damit wird unter Windows nun nicht mehr die
API WinInet, sondern WinHttp benutzt.
Der Fehler:
Wenn WinHttp mit NTLM arbeitet, dann gibt es eine Option, welche steuert, ob es mit den aktuellen Benutzer-Credentials arbeiten soll oder ob man Benutzername/Passwort angeben muss. Diese Option heißt WINHTTP_OPTION_AUTOLOGON_POLICY. Weist man ihr den Wert WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW zu, dann benutzt die Bibliothek den aktuellen Benutzer für den Login und Benutzername/Passwort werden erst gar nicht abgefragt. Weist man der Option hingegen den Wert WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH zu, dann braucht man auf jeden Fall Benutzername/Passwort. In Rio 10.3.1 wurde das überhaupt nicht beachtet, somit war ein Login zum Web Service unter Benutzung des aktuellen Benutzers nicht möglich. In Rio 10.3.2 hat man versucht, das Problem anzugehen und die neue Eigenschaft "useDefaultCredentials" im THTTPClient und im WebNode eingeführt. Dann gibt es folgende Funktion:
Delphi-Quellcode:
procedure TWinHTTPRequest.SetWinLogonPolicy;
var
LClient: TWinHTTPClient;
LOption: DWORD;
begin
LClient := TWinHTTPClient(FClient);
if LClient.UseDefaultCredentials then
LOption := WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW
else
LOption := WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH;
WinHttpSetOption(FWRequest, WINHTTP_OPTION_AUTOLOGON_POLICY, @LOption, SizeOf(LOption));
end;
Sieht eigentlich super aus, funktioniert aber nicht.
Das Problem ist, dass die das "UseDefaultCredentials" nicht einfach im THTTPClient speichern, sondern in einem Record, der wiederum irgendwie in einem Dictionary steckt - da war ich dann irgendwann mit meinem Latein oder meiner Geduld am Ende und es mag sich gerne noch mal jemand ansehen. Der Effekt ist jedenfalls der: man weist zwar in seinem Code dem UseDefaultCredentials ein "true" zu, aber wenn dann die Verbindung tatsächlich initialisiert und somit die obige Funktion aufgerufen wird, dann ist es trotzdem immer noch "false". Die Folge: die Option WINHTTP_OPTION_AUTOLOGON_POLICY wird auf den falschen Wert gesetzt und WinHttp erwartet einen Aufruf von "SetCredentials" mit manuell mitgegebenem Benutzernamen und Passwort.
(hässlicher) Workaround:
Aktuell fällt mir nichts besseres ein, als eine eigene Version der
Unit in mein Projekt zu kopieren und dort die Funktion wie folgt anzupassen:
Delphi-Quellcode:
procedure TWinHTTPRequest.SetWinLogonPolicy;
var
LClient: TWinHTTPClient;
LOption: DWORD;
begin
LClient := TWinHTTPClient(FClient);
if LClient.UseDefaultCredentials or forceSecurityLevelLow then
LOption := WINHTTP_AUTOLOGON_SECURITY_LEVEL_LOW
else
LOption := WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH;
WriteDebugMessage('SetWinLogonPolicy: call WinHttpSetOption with Option '+IntToStr(LOption));
WinHttpSetOption(FWRequest, WINHTTP_OPTION_AUTOLOGON_POLICY, @LOption, SizeOf(LOption));
end;
Der entscheidende Unterschied ist nur
if LClient.UseDefaultCredentials or forceSecurityLevelLow then
. Ich habe hier im interface-Teil noch eine globale Variable ergänzt und kann diese nun vom eigentlichen Programm aus auf true setzen, um das zu erreichen, was ja eigentlich "UseDefaultCredentials" selber tun sollte. Dann klappt ein Login am Server, ohne dass man Benutzername/Passwort irgendwo speichern und angeben muss.
Ich werde dann wohl mal einen Eintrag bei
QC dazu machen...