Wie kann HttpClient dazu gebracht werden, Anmeldeinformationen zusammen mit der Anfrage zu übergeben?

164

Ich habe eine Webanwendung (in IIS gehostet), die mit einem Windows-Dienst kommuniziert. Der Windows-Dienst verwendet die ASP.Net MVC-Web-API (selbst gehostet) und kann daher über JSON über http kommuniziert werden. Die Webanwendung ist für den Identitätswechsel konfiguriert. Die Idee besteht darin, dass der Benutzer, der die Anforderung an die Webanwendung stellt, der Benutzer sein sollte, mit dem die Webanwendung die Anforderung an den Dienst sendet. Die Struktur sieht folgendermaßen aus:

(Der rot hervorgehobene Benutzer ist der Benutzer, auf den in den folgenden Beispielen verwiesen wird.)


Die Webanwendung sendet Anforderungen an den Windows-Dienst mit HttpClient:

var httpClient = new HttpClient(new HttpClientHandler() 
                      {
                          UseDefaultCredentials = true
                      });
httpClient.GetStringAsync("http://localhost/some/endpoint/");

Dadurch wird die Anforderung an den Windows-Dienst gesendet, die Anmeldeinformationen werden jedoch nicht korrekt übergeben (der Dienst meldet den Benutzer als IIS APPPOOL\ASP.NET 4.0). Das will ich nicht .

Wenn ich den obigen Code so ändere, dass WebClientstattdessen a verwendet wird, werden die Anmeldeinformationen des Benutzers korrekt übergeben:

WebClient c = new WebClient
                   {
                       UseDefaultCredentials = true
                   };
c.DownloadStringAsync(new Uri("http://localhost/some/endpoint/"));

Mit dem obigen Code meldet der Dienst den Benutzer als den Benutzer, der die Anforderung an die Webanwendung gestellt hat.

Was mache ich falsch mit der HttpClientImplementierung, die dazu führt, dass die Anmeldeinformationen nicht korrekt übergeben werden (oder liegt ein Fehler mit der vor HttpClient)?

Der Grund, warum ich das verwenden möchte, HttpClientist, dass es eine asynchrone API hat, die gut mit Tasks funktioniert , während die WebClientasyc-API der mit Ereignissen behandelt werden muss.

adrianbanks
quelle
Mögliches Duplikat von stackoverflow.com/q/10308938/1045728
Tommy Grovnes
Es scheint, dass HttpClient und WebClient verschiedene Dinge als DefaultCredentials betrachten. Haben Sie HttpClient.setCredentials (...) ausprobiert?
Germann Arlington
Übrigens hat WebClient DownloadStringTaskAsyncin .Net 4.5, das auch mit async / await verwendet werden kann
LB
1
@GermannArlington: HttpClienthat keine SetCredentials()Methode. Können Sie mich auf das hinweisen, was Sie meinen?
Adrianbanks
4
Es scheint, dass dies behoben wurde (.net 4.5.1)? Ich habe versucht, new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }auf einem Webserver zu erstellen, auf den ein Windows-authentifizierter Benutzer zugreift, und die Website hat sich danach für eine andere Remote-Ressource authentifiziert (würde sich ohne gesetztes Flag nicht authentifizieren).
GSerg

Antworten:

67

Ich hatte auch das gleiche Problem. Ich habe dank der von @tpeczek im folgenden SO-Artikel durchgeführten Recherche eine synchrone Lösung entwickelt: Authentifizierung beim ASP.NET Web Api-Dienst mit HttpClient nicht möglich

Meine Lösung verwendet eine WebClient, die, wie Sie richtig notiert haben, die Anmeldeinformationen ohne Probleme weitergibt. Der Grund, warum HttpClientdies nicht funktioniert, liegt darin, dass die Windows-Sicherheit die Möglichkeit deaktiviert, neue Threads unter einem imitierten Konto zu erstellen (siehe SO-Artikel oben). HttpClientÜber die Task Factory werden neue Threads erstellt, wodurch der Fehler verursacht wird. WebClientAuf der anderen Seite wird synchron auf demselben Thread ausgeführt, wodurch die Regel umgangen und ihre Anmeldeinformationen weitergeleitet werden.

Obwohl der Code funktioniert, ist der Nachteil, dass er nicht asynchron funktioniert.

var wi = (System.Security.Principal.WindowsIdentity)HttpContext.Current.User.Identity;

var wic = wi.Impersonate();
try
{
    var data = JsonConvert.SerializeObject(new
    {
        Property1 = 1,
        Property2 = "blah"
    });

    using (var client = new WebClient { UseDefaultCredentials = true })
    {
        client.Headers.Add(HttpRequestHeader.ContentType, "application/json; charset=utf-8");
        client.UploadData("http://url/api/controller", "POST", Encoding.UTF8.GetBytes(data));
    }
}
catch (Exception exc)
{
    // handle exception
}
finally
{
    wic.Undo();
}

Hinweis: Erfordert das NuGet-Paket: Newtonsoft.Json, das der gleiche JSON-Serializer ist, den WebAPI verwendet.

Joshua
quelle
1
Ich habe am Ende etwas Ähnliches gemacht und es funktioniert wirklich gut. Das asynchrone Problem ist kein Problem, da die Aufrufe blockiert werden sollen.
Adrianbanks
136

Sie können so konfigurieren HttpClient, dass Anmeldeinformationen wie folgt automatisch übergeben werden:

var myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
Sean
quelle
11
Ich weiß, wie das geht. Das Verhalten ist nicht das, was ich möchte (wie in der Frage angegeben) - "Dadurch wird die Anforderung an den Windows-Dienst gesendet, die Anmeldeinformationen werden jedoch nicht korrekt übergeben (der Dienst meldet den Benutzer als IIS APPPOOL \ ASP.NET 4.0) ist nicht das, was ich passieren möchte. "
Adrianbanks
4
Dies scheint mein Problem zu beheben, bei dem nur die Windows-Authentifizierung aktiviert ist. Wenn Sie nur einige legitime Anmeldeinformationen benötigen, sollte dies der Fall sein.
Timmerz
Nicht sicher, ob dies in Identitätswechsel- / Delegierungsszenarien genauso funktioniert wie WebClient. Bei Verwendung von HttpClient mit der oben genannten Lösung wird die Meldung "Der Zielprinzipalname ist falsch" angezeigt. Bei Verwendung von WebClient mit einem ähnlichen Setup werden jedoch die Anmeldeinformationen des Benutzers weitergeleitet.
Peder Rice
Dies hat bei mir funktioniert und die Protokolle zeigen den richtigen Benutzer. Mit Double Hop im Bild hatte ich zwar nicht erwartet, dass es mit NTLM als zugrunde liegendem Authentifizierungsschema funktioniert, aber es funktioniert.
Nitin Rastogi
Wie mache ich dasselbe mit der neuesten Version von Aspnet Core? (2.2). Wenn jemand weiß ...
Nico
26

Sie versuchen, NTLM dazu zu bringen, die Identität an den nächsten Server weiterzuleiten, was nicht möglich ist. Es kann nur einen Identitätswechsel durchführen, bei dem Sie nur auf lokale Ressourcen zugreifen können. Sie können keine Maschinengrenze überschreiten. Die Kerberos-Authentifizierung unterstützt die Delegierung (was Sie benötigen) mithilfe von Tickets. Das Ticket kann weitergeleitet werden, wenn alle Server und Anwendungen in der Kette korrekt konfiguriert sind und Kerberos in der Domäne korrekt eingerichtet ist. Kurz gesagt, Sie müssen von NTLM zu Kerberos wechseln.

Weitere Informationen zu den Windows-Authentifizierungsoptionen und deren Funktionsweise finden Sie unter: http://msdn.microsoft.com/en-us/library/ff647076.aspx

BlackSpy
quelle
3
" NTLM, um die Identität an den nächsten Server weiterzuleiten, was nicht möglich ist " - wie kommt es, dass dies bei der Verwendung geschieht WebClient? Das ist das , was ich nicht verstehe - wenn es nicht möglich ist, wie kommt es ist , es zu tun?
Adrianbanks
2
Bei Verwendung des Webclients besteht immer noch nur eine Verbindung zwischen dem Client und dem Server. Es kann sich als Benutzer auf diesem Server ausgeben (1 Hop), diese Anmeldeinformationen jedoch nicht an einen anderen Computer weiterleiten (2 Hops - Client an Server an 2. Server). Dafür benötigen Sie eine Delegierung.
BlackSpy
1
Die einzige Möglichkeit, das zu erreichen, was Sie versuchen, besteht darin, den Benutzer dazu zu bringen, seinen Benutzernamen und sein Kennwort in ein benutzerdefiniertes Dialogfeld in Ihrer ASP.NET-Anwendung einzugeben, sie als Zeichenfolgen zu speichern und dann zu verwenden Sie können Ihre Identität festlegen, wenn Sie eine Verbindung zu Ihrem Web-API-Projekt herstellen. Andernfalls müssen Sie NTLM löschen und zu Kerberos wechseln, damit Sie das Kerboros-Ticket an das Web-API-Projekt übergeben können. Ich empfehle dringend, den Link zu lesen, den ich in meiner ursprünglichen Antwort angehängt habe. Was Sie versuchen, erfordert ein gründliches Verständnis der Windows-Authentifizierung, bevor Sie beginnen.
BlackSpy
2
@BlackSpy: Ich habe viel Erfahrung mit der Windows-Authentifizierung. Ich versuche zu verstehen, warum das WebClientdie NTLM-Anmeldeinformationen weitergeben kann, das HttpClientaber nicht. Ich kann dies nur mit dem ASP.Net-Identitätswechsel erreichen, ohne Kerberos verwenden oder Benutzernamen / Kennwörter speichern zu müssen. Dies funktioniert jedoch nur mit WebClient.
Adrianbanks
1
Es sollte unmöglich sein, sich über mehr als einen Hop hinweg auszugeben, ohne den Benutzernamen und das Passwort als Text weiterzugeben. Es verstößt gegen die Regeln des Identitätswechsels, und NTLM lässt dies nicht zu. Mit WebClient können Sie 1 Sprung springen, da Sie die Anmeldeinformationen übergeben und als dieser Benutzer auf der Box ausgeführt werden. Wenn Sie sich die Sicherheitsprotokolle ansehen, sehen Sie die Anmeldung - der Benutzer meldet sich beim System an. Sie können dann nicht als dieser Benutzer von diesem Computer aus ausgeführt werden, es sei denn, Sie haben die Anmeldeinformationen als Text übergeben und verwenden eine andere Webclient-Instanz, um sich beim nächsten Feld anzumelden.
BlackSpy
17

OK, also danke an alle oben genannten Mitwirkenden. Ich verwende .NET 4.6 und wir hatten auch das gleiche Problem. Ich verbrachte Zeit mit dem Debuggen System.Net.Http, insbesondere dem HttpClientHandler, und fand Folgendes:

    if (ExecutionContext.IsFlowSuppressed())
    {
      IWebProxy webProxy = (IWebProxy) null;
      if (this.useProxy)
        webProxy = this.proxy ?? WebRequest.DefaultWebProxy;
      if (this.UseDefaultCredentials || this.Credentials != null || webProxy != null && webProxy.Credentials != null)
        this.SafeCaptureIdenity(state);
    }

Nachdem ExecutionContext.IsFlowSuppressed()ich festgestellt hatte , dass dies möglicherweise der Schuldige war, habe ich unseren Identitätswechselcode wie folgt verpackt:

using (((WindowsIdentity)ExecutionContext.Current.Identity).Impersonate())
using (System.Threading.ExecutionContext.SuppressFlow())
{
    // HttpClient code goes here!
}

Der Code in SafeCaptureIdenity(nicht mein Rechtschreibfehler) greift WindowsIdentity.Current()nach unserer Identität. Dies wird aufgegriffen, weil wir jetzt den Fluss unterdrücken. Aufgrund der Verwendung / Entsorgung wird diese nach dem Aufruf zurückgesetzt.

Es scheint jetzt für uns zu funktionieren, Puh!

Chullybun
quelle
2
Vielen Dank für diese Analyse. Dies hat auch meine Situation behoben. Jetzt wird meine Identität korrekt an die andere Webanwendung weitergegeben! Du hast mir Stunden Arbeit erspart! Ich bin überrascht, dass die Zeckenzahl nicht höher ist.
justdan23
Ich brauchte nur using (System.Threading.ExecutionContext.SuppressFlow())und das Problem wurde für mich gelöst!
ZX9
10

In .NET Kern konnte ich eine bekommen System.Net.Http.HttpClientmit UseDefaultCredentials = truedem authentifizierten Benutzer-Windows - Anmeldeinformationen an einen Back - End - Dienst durchlaufen , indem Sie WindowsIdentity.RunImpersonated.

HttpClient client = new HttpClient(new HttpClientHandler { UseDefaultCredentials = true } );
HttpResponseMessage response = null;

if (identity is WindowsIdentity windowsIdentity)
{
    await WindowsIdentity.RunImpersonated(windowsIdentity.AccessToken, async () =>
    {
        var request = new HttpRequestMessage(HttpMethod.Get, url)
        response = await client.SendAsync(request);
    });
}
Geißel192
quelle
4

Es hat bei mir funktioniert, nachdem ich einen Benutzer mit Internetzugang im Windows-Dienst eingerichtet habe.

In meinem Code:

HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = System.Net.WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
.....
HttpClient httpClient = new HttpClient(handler)
.... 
Paulo Augusto Batista
quelle
3

Ok, also habe ich Joshoun-Code genommen und ihn generisch gemacht. Ich bin nicht sicher, ob ich ein Singleton-Muster für die SynchronousPost-Klasse implementieren soll. Vielleicht kann jemand mit mehr Wissen helfen.

Implementierung

// Ich nehme an, Sie haben Ihren eigenen konkreten Typ. In meinem Fall verwende ich zuerst Code mit einer Klasse namens FileCategory

FileCategory x = new FileCategory { CategoryName = "Some Bs"};
SynchronousPost<FileCategory>test= new SynchronousPost<FileCategory>();
test.PostEntity(x, "/api/ApiFileCategories"); 

Generische Klasse hier. Sie können jeden Typ übergeben

 public class SynchronousPost<T>where T :class
    {
        public SynchronousPost()
        {
            Client = new WebClient { UseDefaultCredentials = true };
        }

        public void PostEntity(T PostThis,string ApiControllerName)//The ApiController name should be "/api/MyName/"
        {
            //this just determines the root url. 
            Client.BaseAddress = string.Format(
         (
            System.Web.HttpContext.Current.Request.Url.Port != 80) ? "{0}://{1}:{2}" : "{0}://{1}",
            System.Web.HttpContext.Current.Request.Url.Scheme,
            System.Web.HttpContext.Current.Request.Url.Host,
            System.Web.HttpContext.Current.Request.Url.Port
           );
            Client.Headers.Add(HttpRequestHeader.ContentType, "application/json;charset=utf-8");
            Client.UploadData(
                                 ApiControllerName, "Post", 
                                 Encoding.UTF8.GetBytes
                                 (
                                    JsonConvert.SerializeObject(PostThis)
                                 )
                             );  
        }
        private WebClient Client  { get; set; }
    }

Meine Api-Klassen sehen so aus, wenn Sie neugierig sind

public class ApiFileCategoriesController : ApiBaseController
{
    public ApiFileCategoriesController(IMshIntranetUnitOfWork unitOfWork)
    {
        UnitOfWork = unitOfWork;
    }

    public IEnumerable<FileCategory> GetFiles()
    {
        return UnitOfWork.FileCategories.GetAll().OrderBy(x=>x.CategoryName);
    }
    public FileCategory GetFile(int id)
    {
        return UnitOfWork.FileCategories.GetById(id);
    }
    //Post api/ApileFileCategories

    public HttpResponseMessage Post(FileCategory fileCategory)
    {
        UnitOfWork.FileCategories.Add(fileCategory);
        UnitOfWork.Commit(); 
        return new HttpResponseMessage();
    }
}

Ich benutze Ninject und Repo-Muster mit Arbeitseinheit. Wie auch immer, die obige generische Klasse hilft wirklich.

versteckt
quelle