Jak sprawić, by HttpClient przekazał poświadczenia wraz z żądaniem?


164

Mam aplikację internetową (hostowaną w IIS), która komunikuje się z usługą systemu Windows. Usługa systemu Windows korzysta z interfejsu API sieci Web ASP.Net MVC (samodzielnie hostowana), więc można ją komunikować za pośrednictwem protokołu HTTP przy użyciu formatu JSON. Aplikacja internetowa jest skonfigurowana do podszywania się, a idea polega na tym, że użytkownik, który wysyła żądanie do aplikacji internetowej, powinien być użytkownikiem, którego aplikacja internetowa używa do wysłania żądania do usługi. Struktura wygląda następująco:

(Użytkownik wyróżniony na czerwono to użytkownik, o którym mowa w poniższych przykładach).


Aplikacja internetowa wysyła żądania do usługi Windows za pomocą HttpClient:

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

Powoduje to wysłanie żądania do usługi systemu Windows, ale nie przekazuje poprawnie poświadczeń (usługa zgłasza użytkownika jako IIS APPPOOL\ASP.NET 4.0). Nie tego chcę .

Jeśli zmienię powyższy kod, aby WebClientzamiast tego używał a, poświadczenia użytkownika są przekazywane poprawnie:

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

Za pomocą powyższego kodu usługa zgłasza użytkownika jako użytkownika, który zgłosił żądanie do aplikacji internetowej.

Co robię źle z HttpClientimplementacją, która powoduje, że nie przekazuje ona poprawnie poświadczeń (lub czy jest to błąd z HttpClient)?

Powodem, dla którego chcę użyć HttpClientjest to, że ma async API, które działa dobrze z Tasks, podczas gdy WebClientasyc API musi być obsługiwane ze zdarzeniami.



Wygląda na to, że HttpClient i WebClient traktują różne rzeczy jako DefaultCredentials. Czy próbowałeś HttpClient.setCredentials (...)?
Germann Arlington

BTW, WebClient ma DownloadStringTaskAsyncw .Net 4.5, który może być również używany z async / await
LB

1
@GermannArlington: HttpClientnie ma SetCredentials()metody. Czy możesz wskazać mi, co masz na myśli?
adrianbanks

4
Wygląda na to, że problem został rozwiązany (.net 4.5.1)? Próbowałem utworzyć new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }na serwerze sieciowym, do którego dostęp miał użytkownik uwierzytelniony w systemie Windows, a następnie witryna sieci Web uwierzytelniła się dla innego zdalnego zasobu (nie uwierzytelniłaby się bez ustawionej flagi).
GSerg

Odpowiedzi:


67

Ja też miałem ten sam problem. Opracowałem rozwiązanie synchroniczne dzięki badaniom wykonanym przez @tpeczek w następującym artykule SO: Nie można uwierzytelnić się w usłudze ASP.NET Web Api z HttpClient

Moje rozwiązanie wykorzystuje znak WebClient, który, jak poprawnie zauważyłeś, przekazuje poświadczenia bez problemu. Przyczyną HttpClientnie jest to, że zabezpieczenia systemu Windows wyłączają możliwość tworzenia nowych wątków na podszywanym koncie (zobacz artykuł SO powyżej). HttpClientTworzy nowe wątki za pośrednictwem fabryki zadań, powodując błąd. WebClientz drugiej strony działa synchronicznie w tym samym wątku, pomijając w ten sposób regułę i przekazując jej poświadczenia.

Chociaż kod działa, wadą jest to, że nie będzie działać asynchronicznie.

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

Uwaga: wymaga pakietu NuGet: Newtonsoft.Json, który jest tym samym, z którego korzysta interfejs WebAPI serializatora JSON.


1
W końcu zrobiłem coś podobnego i działa to naprawdę dobrze. Problem asynchroniczny nie stanowi problemu, ponieważ chcę, aby wywołania były blokowane.
adrianbanks

136

Możesz skonfigurować HttpClientautomatyczne przekazywanie poświadczeń w następujący sposób:

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

11
Wiem, jak to zrobić. To zachowanie nie jest tym, czego chcę (jak stwierdzono w pytaniu) - „To powoduje wysłanie żądania do usługi systemu Windows, ale nie przekazuje poprawnie poświadczeń (usługa zgłasza użytkownika jako IIS APPPOOL \ ASP.NET 4.0). nie jest tym, czego chcę ”.
adrianbanks

4
wydaje się, że rozwiązuje to mój problem, w którym iis ma włączone tylko uwierzytelnianie systemu Windows. jeśli potrzebujesz tylko niektórych legalnych poświadczeń, powinno to wystarczyć.
Timmerz

Nie jestem pewien, czy działa to tak samo jak WebClient w scenariuszach personifikacji / delegowania. Otrzymuję komunikat „Docelowa nazwa główna jest nieprawidłowa”, gdy używam HttpClient z powyższym rozwiązaniem, ale używanie WebClient z podobną konfiguracją przekazuje poświadczenia użytkownika.
Peder Rice

To zadziałało dla mnie, a dzienniki pokazują prawidłowego użytkownika. Chociaż z podwójnym przeskokiem na obrazie nie spodziewałem się, że będzie działać z NTLM jako podstawowym schematem uwierzytelniania, ale działa.
Nitin Rastogi

Jak zrobić to samo, korzystając z najnowszej wersji rdzenia aspnet? (2.2). Jeśli ktoś wie ...
Nico

26

To, co próbujesz zrobić, to zmusić NTLM do przekazania tożsamości na następny serwer, czego nie może zrobić - może wykonać tylko personifikację, która zapewnia tylko dostęp do zasobów lokalnych. Nie pozwoli ci przekroczyć granicy maszyny. Uwierzytelnianie Kerberos obsługuje delegowanie (to, czego potrzebujesz) przy użyciu biletów, a bilet można przekazać dalej, gdy wszystkie serwery i aplikacje w łańcuchu są poprawnie skonfigurowane, a protokół Kerberos jest poprawnie skonfigurowany w domenie. Krótko mówiąc, musisz przejść z używania NTLM na Kerberos.

Więcej informacji na temat dostępnych opcji uwierzytelniania systemu Windows i sposobu ich działania można znaleźć pod adresem : http://msdn.microsoft.com/en-us/library/ff647076.aspx


3
NTLM, aby przekazać tożsamość na następny serwer, czego nie może zrobić ” - jak to się dzieje, gdy używa WebClient? To jest rzecz, której nie rozumiem - jeśli nie jest to możliwe, jak to się stało, że się to robi?
adrianbanks

2
W przypadku korzystania z klienta internetowego jest to nadal tylko jedno połączenie między klientem a serwerem. Może podszywać się pod użytkownika na tym serwerze (1 przeskok), ale nie może przekazywać tych danych uwierzytelniających na inną maszynę (2 przeskoki - klient do serwera na drugi serwer). Do tego potrzebujesz delegacji.
BlackSpy

1
Jedynym sposobem na wykonanie tego, co próbujesz zrobić w sposób, w jaki próbujesz to zrobić, jest skłonienie użytkownika do wpisania swojej nazwy użytkownika i hasła w niestandardowym oknie dialogowym w aplikacji ASP.NET, zapisanie ich jako ciągów znaków, a następnie użycie aby ustawić swoją tożsamość podczas łączenia się z projektem interfejsu API sieci Web. W przeciwnym razie musisz porzucić NTLM i przejść do Kerberos, aby móc przekazać bilet Kerboros do projektu interfejsu API sieci Web. Gorąco polecam przeczytanie linku, który załączyłem w mojej oryginalnej odpowiedzi. To, co próbujesz zrobić, wymaga dobrego zrozumienia uwierzytelniania systemu Windows przed rozpoczęciem.
BlackSpy

2
@BlackSpy: Mam duże doświadczenie z uwierzytelnianiem systemu Windows. Próbuję zrozumieć, dlaczego WebClientmoże przekazać poświadczenia NTLM, ale HttpClientnie może. Mogę to osiągnąć za pomocą samej personifikacji ASP.Net, bez konieczności używania protokołu Kerberos lub przechowywania nazw użytkowników / haseł. Działa to jednak tylko z WebClient.
adrianbanks,

1
Podszywanie się pod więcej niż jeden przeskok powinno być niemożliwe bez przekazywania nazwy użytkownika i hasła w postaci tekstu. łamie zasady podszywania się pod inne osoby, a NTLM na to nie zezwala. WebClient umożliwia przeskoczenie o 1 przeskok, ponieważ pomijasz poświadczenia i uruchamiasz jako ten użytkownik w pudełku. Jeśli spojrzysz na logi bezpieczeństwa, zobaczysz login - użytkownik loguje się do systemu. Nie możesz wtedy działać jako ten użytkownik z tego komputera, chyba że przekażesz poświadczenia jako tekst i użyjesz innej instancji klienta sieci Web, aby zalogować się do następnego pola.
BlackSpy

17

OK, dziękuję wszystkim współautorom powyżej. Używam .NET 4.6 i mieliśmy ten sam problem. Spędziłem czas na debugowaniu System.Net.Http, a konkretnie na HttpClientHandler, i znalazłem:

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

Więc po ocenie, że ExecutionContext.IsFlowSuppressed()mógł być winowajcą, opakowałem nasz kod personifikacji w następujący sposób:

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

Kod wewnątrz SafeCaptureIdenity(nie mój błąd w pisowni), chwyta WindowsIdentity.Current()naszą podrobioną tożsamość. Jest to zauważane, ponieważ teraz tłumimy przepływ. Ze względu na użycie / dispose jest to resetowane po wywołaniu.

Teraz wydaje się, że to działa dla nas, uff!


2
Dziękuję bardzo za wykonanie tej analizy. To też naprawiło moją sytuację. Teraz moja tożsamość jest poprawnie przekazywana do innej aplikacji internetowej! Zaoszczędziłeś mi godzin pracy! Dziwię się, że nie jest wyższa liczba kleszczy.
justdan23

Potrzebowałem tylko using (System.Threading.ExecutionContext.SuppressFlow())i problem został rozwiązany dla mnie!
ZX9

10

W .NET Core udało mi się uzyskać System.Net.Http.HttpClientz, UseDefaultCredentials = trueaby przekazać poświadczenia systemu Windows uwierzytelnionego użytkownika do usługi zaplecza przy użyciu 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

U mnie zadziałało po skonfigurowaniu użytkownika z dostępem do Internetu w usłudze Windows.

W moim kodzie:

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

3

Ok, więc wziąłem kod Joshouna i uczyniłem go ogólnym. Nie jestem pewien, czy powinienem implementować wzorzec singleton w klasie SynchronousPost. Może ktoś lepiej poinformowany może pomóc.

Realizacja

// Zakładam, że masz swój własny konkretny typ. W moim przypadku najpierw używam kodu z klasą o nazwie FileCategory

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

Klasa ogólna tutaj. Możesz przekazać dowolny typ

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

Moje klasy Api wyglądają tak, jeśli jesteś ciekawy

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

Używam ninject i wzorca repo z jednostką pracy. W każdym razie powyższa klasa ogólna naprawdę pomaga.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.