Come fare in modo che HttpClient passi le credenziali insieme alla richiesta?


164

Ho un'applicazione Web (ospitata in IIS) che parla con un servizio Windows. Il servizio Windows utilizza l'API Web ASP.Net MVC (self-hosted) e pertanto può essere comunicato tramite http tramite JSON. L'applicazione Web è configurata per eseguire la rappresentazione, con l'idea che l'utente che effettua la richiesta all'applicazione Web dovrebbe essere l'utente che l'applicazione Web utilizza per effettuare la richiesta al servizio. La struttura si presenta così:

(L'utente evidenziato in rosso è l'utente a cui si fa riferimento negli esempi seguenti.)


L'applicazione Web invia richieste al servizio Windows utilizzando un HttpClient:

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

Questo invia la richiesta al servizio Windows, ma non trasferisce correttamente le credenziali (il servizio riporta l'utente come IIS APPPOOL\ASP.NET 4.0). Non è quello che voglio succedere .

Se cambio invece il codice sopra per utilizzare un WebClient, le credenziali dell'utente vengono passate correttamente:

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

Con il codice sopra riportato, il servizio segnala l'utente come l'utente che ha effettuato la richiesta all'applicazione Web.

Cosa sto facendo di sbagliato HttpClientnell'implementazione che sta causando il mancato passaggio corretto delle credenziali (o è un bug con HttpClient)?

Il motivo per cui voglio usare HttpClientè che ha un'API asincrona che funziona bene con Tasks, mentre l' WebClientAPI asyc deve essere gestita con eventi.



Sembra che HttpClient e WebClient considerino cose diverse come DefaultCredentials. Hai provato HttpClient.setCredentials (...)?
Germann Arlington,

A proposito, WebClient ha DownloadStringTaskAsyncin .Net 4.5, che può anche essere usato con async / await
LB

1
@GermannArlington: HttpClientnon ha un SetCredentials()metodo. Puoi indicarmi cosa intendi?
adrianbanks,

4
Sembrerebbe che questo sia stato corretto (.net 4.5.1)? Ho provato a creare new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }su un server Web a cui accede un utente autenticato da Windows, e il sito Web si è autenticato per un'altra risorsa remota successivamente (non si autenticherà senza il flag impostato).
GSerg,

Risposte:


67

Avevo anche questo stesso problema. Ho sviluppato una soluzione sincrona grazie alla ricerca condotta da @tpeczek nel seguente articolo SO: Impossibile eseguire l'autenticazione al servizio Api Web ASP.NET con HttpClient

La mia soluzione usa a WebClient, che come hai notato correttamente passa le credenziali senza problemi. Il motivo HttpClientnon funziona perché la sicurezza di Windows disabilita la possibilità di creare nuovi thread con un account rappresentato (vedere l'articolo SO sopra.) HttpClientCrea nuovi thread tramite Task Factory causando così l'errore. WebClientd'altra parte, viene eseguito in modo sincrono sullo stesso thread ignorando così la regola e inoltrandone le credenziali.

Sebbene il codice funzioni, il rovescio della medaglia è che non funzionerà in modo asincrono.

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();
}

Nota: richiede il pacchetto NuGet: Newtonsoft.Json, che è lo stesso serializzatore JSON utilizzato da WebAPI.


1
Alla fine ho fatto qualcosa di simile e funziona davvero bene. Il problema asincrono non è un problema, poiché desidero bloccare le chiamate.
adrianbanks,

136

È possibile configurare HttpClientper passare automaticamente le credenziali in questo modo:

var myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });

11
So come farlo. Il comportamento non è quello che desidero (come indicato nella domanda): "Questo invia la richiesta al servizio Windows, ma non trasferisce correttamente le credenziali (il servizio riporta l'utente come IIS APPPOOL \ ASP.NET 4.0). Questo non è quello che voglio succedere ".
adrianbanks,

4
questo sembra risolvere il mio problema in cui iis ha solo l'autenticazione di Windows abilitata. se hai solo bisogno di passare alcune credenziali legittime, questo dovrebbe farlo.
Timmerz,

Non sono sicuro che funzioni allo stesso modo di WebClient in scenari di rappresentazione / delega. Ottengo "Il nome principale di destinazione non è corretto" quando si utilizza HttpClient con la soluzione sopra, ma l'utilizzo di WebClient con un'impostazione simile passa le credenziali dell'utente.
Peder Rice

Questo ha funzionato per me e i registri mostrano l'utente corretto. Anche se, con il doppio hop nella foto, non mi aspettavo che funzionasse con NTLM come schema di autenticazione sottostante, ma funziona.
Nitin Rastogi,

Come fare lo stesso usando l'ultima versione di aspnet core? (2.2). Se qualcuno lo sa ...
Nico,

26

Quello che stai cercando di fare è convincere NTLM a inoltrare l'identità al server successivo, cosa che non può fare: può solo fare la rappresentazione che ti dà solo accesso alle risorse locali. Non ti permetterà di attraversare un confine macchina. L'autenticazione Kerberos supporta la delega (ciò di cui hai bisogno) utilizzando i ticket e il ticket può essere inoltrato quando tutti i server e le applicazioni della catena sono configurati correttamente e Kerberos è impostato correttamente sul dominio. Quindi, in breve, è necessario passare dall'utilizzo di NTLM a Kerberos.

Per ulteriori informazioni sulle opzioni di autenticazione di Windows disponibili e su come funzionano, iniziare da: http://msdn.microsoft.com/en-us/library/ff647076.aspx


3
" NTLM per inoltrare l'identità al server successivo, cosa che non può fare " - come mai lo fa quando lo utilizza WebClient? Questa è la cosa che non capisco: se non è possibile, come mai lo sta facendo?
adrianbanks,

2
Quando si utilizza il client Web è ancora solo una connessione, tra il client e il server. Può impersonare l'utente su quel server (1 hop), ma non può inoltrare quelle credenziali su un altro computer (2 hop - da client a server al secondo server). Per questo è necessaria la delega.
BlackSpy,

1
L'unico modo per realizzare ciò che si sta tentando di fare nel modo in cui si sta tentando di farlo è indurre l'utente a digitare il nome utente e la password in una finestra di dialogo personalizzata sull'applicazione ASP.NET, archiviarli come stringhe e quindi utilizzare per impostare la tua identità quando ti connetti al tuo progetto API Web. In caso contrario, è necessario eliminare NTLM e passare a Kerberos, in modo da poter passare il ticket Kerboros al progetto API Web. Consiglio vivamente di leggere il link che ho allegato nella mia risposta originale. Quello che stai cercando di fare richiede una conoscenza approfondita dell'autenticazione di Windows prima di iniziare.
BlackSpy,

2
@ BlackSpy: ho molta esperienza con l'autenticazione di Windows. Quello che sto cercando di capire è il motivo per cui WebClientpuò trasmettere le credenziali NTLM, ma HttpClientnon può. Posso ottenere ciò utilizzando la sola rappresentazione di ASP.Net e non dover utilizzare Kerberos o archiviare nomi utente / password. Questo tuttavia funziona solo con WebClient.
adrianbanks,

1
Dovrebbe essere impossibile impersonare più di 1 hop senza passare il nome utente e la password come testo. infrange le regole della rappresentazione e NTLM non lo consentirà. WebClient ti consente di saltare 1 hop perché passi le credenziali ed esegui come quell'utente sulla scatola. Se guardi i registri di sicurezza vedrai il login - l'utente accede al sistema. Non è quindi possibile eseguire come tale utente da quella macchina a meno che non si siano passate le credenziali come testo e non si usi un'altra istanza del client Web per accedere alla casella successiva.
BlackSpy,

17

OK, quindi grazie a tutti i collaboratori sopra. Sto usando .NET 4.6 e abbiamo avuto anche lo stesso problema. Ho impiegato del tempo per il debug System.Net.Http, in particolare il HttpClientHandler, e ho trovato quanto segue:

    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);
    }

Quindi, dopo aver valutato che ExecutionContext.IsFlowSuppressed()potrebbe essere stato il colpevole, ho concluso il nostro codice di rappresentazione come segue:

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

Il codice all'interno di SafeCaptureIdenity(non il mio errore di ortografia), afferra WindowsIdentity.Current()quale è la nostra identità impersonata. Questo è stato raccolto perché ora stiamo sopprimendo il flusso. A causa dell'utilizzo / eliminazione, questo viene ripristinato dopo l'invocazione.

Ora sembra funzionare per noi, accidenti!


2
Grazie mille per aver fatto questa analisi. Ciò ha risolto anche la mia situazione. Ora la mia identità viene trasmessa correttamente all'altra applicazione Web! Mi hai risparmiato ore di lavoro! Sono sorpreso che non sia più alto sul numero di tick.
justdan23,

Ne avevo solo bisogno using (System.Threading.ExecutionContext.SuppressFlow())e il problema è stato risolto per me!
ZX9

10

In .NET Core, sono riuscito a ottenere un System.Net.Http.HttpClientcon UseDefaultCredentials = trueper passare le credenziali di Windows dell'utente autenticato a un servizio back-end utilizzando 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);
    });
}

4

Ha funzionato per me dopo aver impostato un utente con accesso a Internet nel servizio Windows.

Nel mio codice:

HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = System.Net.WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
.....
HttpClient httpClient = new HttpClient(handler)
.... 

3

Ok, ho preso il codice Joshoun e l'ho reso generico. Non sono sicuro se dovrei implementare il modello singleton sulla classe SynchronousPost. Forse qualcuno più competente può aiutare.

Implementazione

// Presumo che tu abbia il tuo tipo concreto. Nel mio caso sto usando prima il codice con una classe chiamata FileCategory

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

Classe generica qui. Puoi passare qualsiasi tipo

 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; }
    }

Le mie lezioni di Api sono così, se sei curioso

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();
    }
}

Sto usando ninject e il modello di pronti contro termine con l'unità di lavoro. Comunque, la classe generica sopra aiuta davvero.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.