Як змусити HttpClient передавати облікові дані разом із запитом?


164

У мене є веб-додаток (розміщений в IIS), який спілкується зі службою Windows. Служба Windows використовує веб-API ASP.Net MVC (власний хостинг), і тому можна спілкуватися з http через JSON. Веб-додаток налаштовано на видання себе, ідея полягає в тому, що користувач, який робить запит на веб-додаток, повинен бути користувачем, який використовує веб-додаток для подання запиту в сервіс. Структура виглядає так:

(Користувачем, виділеним червоним кольором, є користувач, про який йдеться у прикладах нижче.)


Веб-додаток робить запити до служби Windows за допомогою HttpClient:

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

Це робить запит до служби Windows, але не передає облікові дані правильно (сервіс повідомляє користувача як IIS APPPOOL\ASP.NET 4.0). Це не те, що я хочу, щоб сталося .

Якщо я зміню вищевказаний код, щоб використовувати WebClientзамість нього, облікові дані користувача передаються правильно:

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

За допомогою наведеного вище коду сервіс повідомляє користувача як користувача, який зробив запит у веб-програмі.

Що я роблю неправильно з HttpClientреалізацією, яка змушує її неправильно передавати облікові дані (чи це помилка з HttpClient)?

Причиною, для якої я хочу скористатися, HttpClientє те, що він має API асинхронізації, який добре працює з Tasks, тоді як WebClientAPI asyc API потрібно обробляти подіями.


Можливий дублікат stackoverflow.com/q/10308938/1045728
Tommy Grovnes

Схоже, що HttpClient і WebClient вважають різні речі за замовчуванням. Ви спробували HttpClient.setCredentials (...)?
Герман Арлінгтон

BTW, WebClient має DownloadStringTaskAsync.Net 4.5, який також можна використовувати з асинхронізуванням / очікувати
LB

1
@GermannArlington: HttpClientне має SetCredentials()методу. Чи можете ви вказати мені на те, що ви маєте на увазі?
adrianbanks

4
Здається, це виправлено (.net 4.5.1)? Я спробував створити new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }на веб-сервері, до якого звертався користувач із автентифікацією Windows, і веб-сайт після цього здійснив автентифікацію для іншого віддаленого ресурсу (не встановив автентифікацію без встановленого прапора).
GSerg

Відповіді:


67

У мене теж була ця сама проблема. Я розробив синхронне рішення завдяки дослідженню, проведеному @tpeczek, у наступній статті SO: Неможливо встановити автентифікацію на сервісі ASP.NET Web Api з HttpClient

У моєму рішенні використовується a WebClient, який, як ви правильно зазначили, передає облікові дані без проблем. Причина HttpClientне працює в тому, що захист Windows відключає можливість створювати нові потоки під іменованим обліковим записом (див. Статтю SO вище.) HttpClientСтворює нові потоки через Фабрику завдань, тим самим спричиняючи помилку. WebClientз іншого боку, працює синхронно на одному потоці, тим самим обходячи правило і пересилаючи його облікові дані.

Хоча код працює, недоліком є ​​те, що він не працюватиме асинхронізуванням.

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

Примітка. Потрібен пакет NuGet: Newtonsoft.Json, який є тим же серіалізатором JSON, який використовує WebAPI.


1
Зрештою я зробив щось подібне, і це працює дуже добре. Асинхронна проблема не є проблемою, тому що я хочу блокувати дзвінки.
adrianbanks

136

Ви можете налаштувати HttpClientавтоматично передавати такі дані:

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

11
Я знаю, як це зробити. Поведінка - це не те, чого я хочу (як зазначено у запитанні) - "Це робить запит до служби Windows, але не передає облікові дані правильно (сервіс повідомляє користувача як IIS APPPOOL \ ASP.NET 4.0). Це це не те, що я хочу, щоб це сталося ».
adrianbanks

4
це, здається, вирішує мою проблему, коли у iis увімкнено лише автентифікацію Windows. якщо вам просто потрібні певні легітимні документи, це потрібно зробити.
Тіммерц

Не впевнений, що це працює так само, як і WebClient у сценаріях імперсонації / делегування. Я отримую "Цільове головне ім'я невірно", коли використовую HttpClient з вищевказаним рішенням, але за допомогою WebClient з подібною установкою передає облікові дані користувача.
Педер Райс

Це працювало для мене, і журнали показують правильного користувача. Хоча, маючи подвійний скачок на знімку, я не очікував, що він буде працювати з NTLM як основної схеми аутентифікації, але вона працює.
Нітін Растогі

Як зробити те саме, використовуючи останню версію ядра aspnet? (2.2). Якщо хтось знає ...
Ніко

26

Те, що ви намагаєтеся зробити, це змусити NTLM переслати ідентифікацію на наступний сервер, що він не може зробити - він може робити лише себе, що дає лише доступ до місцевих ресурсів. Це не дозволить вам перейти межу машини. Аутентифікація Kerberos підтримує делегування (те, що вам потрібно) за допомогою квитків, і квиток можна переадресувати, коли всі сервери та програми в ланцюжку правильно налаштовані і Kerberos налаштований правильно в домені. Отже, коротше кажучи, вам потрібно перейти від використання NTLM до Kerberos.

Докладніші відомості про доступні вам параметри автентифікації Windows та спосіб їх роботи починаються на веб- сайті: http://msdn.microsoft.com/en-us/library/ff647076.aspx


3
" NTLM для пересилання ідентифікації на наступний сервер, що він не може зробити " - як це зробити це під час використання WebClient? Це річ , яку я не розумію - якщо це не представляється можливим, як же він буде робити це?
adrianbanks

2
Під час використання веб-клієнта все ще залишається лише одне з'єднання між клієнтом і сервером. Він може видавати себе за користувача на цьому сервері (1 хоп), але не може переслати ці дані на іншу машину (2 перестрибування - клієнт на сервер на 2 сервер). Для цього вам потрібна делегація.
BlackSpy

1
Єдиний спосіб зробити те, що ви намагаєтеся зробити так, як ви це намагаєтеся, - це змусити користувача ввести своє ім’я користувача та пароль у спеціальне діалогове вікно у вашому додатку ASP.NET, зберегти їх у вигляді рядків і потім використовувати щоб встановити вашу особу під час підключення до проекту Web API. Інакше вам потрібно скинути NTLM і перейти до Kerberos, щоб ви могли передати квиток Kerboros через проект Web API. Я настійно рекомендую прочитати посилання, яке я додав у своїй оригінальній відповіді. Те, що ви намагаєтеся зробити, вимагає чіткого розуміння автентифікації Windows перед початком роботи.
BlackSpy

2
@BlackSpy: Я маю багато досвіду роботи з автентифікацією Windows. Я намагаюся зрозуміти, це чому WebClientможе передавати облікові дані NTLM, але HttpClientне можна. Я можу домогтися цього, використовуючи лише себе, видаючи себе за ASP.Net, і не потрібно використовувати Kerberos або зберігати імена користувачів / паролі. Це, однак, працює тільки з WebClient.
adrianbanks

1
Виявляти себе за більш ніж 1 стрибок не можна, не передаючи ім'я користувача та пароль навколо як текст. він порушує правила себе, і NTLM не дозволить цього зробити. WebClient дозволяє перестрибувати 1 стрибок, оскільки ви передаєте облікові дані та біжите як цей користувач у вікні. Якщо ви подивитесь на журнали безпеки, ви побачите логін - користувач увійде в систему. Тоді ви не можете запустити цього користувача на цій машині, якщо ви не передали облікові дані як текст і не використовуєте інший екземпляр веб-клієнта для входу в наступне поле.
BlackSpy

17

Гаразд, тому дякую всім учасникам, що вказують вище. Я використовую .NET 4.6, і у нас також була така ж проблема. Я витратив час налагодження System.Net.Http, зокрема HttpClientHandler, і виявив наступне:

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

Отож, оцінивши, що це, ExecutionContext.IsFlowSuppressed()можливо, був винувателем, я обклав наш код себеління наступним чином:

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

Код всередині SafeCaptureIdenity(не моя орфографічна помилка), хапає WindowsIdentity.Current()який є нашою підробленою особою. Це збирається, тому що ми зараз пригнічуємо потік. Через використання / розпорядження це скидається після виклику.

Зараз це, здається, працює на нас, еге!


2
Дякую вам дуже багато за те, що зробили цей аналіз. Це і виправило мою ситуацію. Тепер моя особистість правильно передається іншому веб-додатку! Ти врятував мені години роботи! Я здивований, що це не вище кількість кліщів.
justdan23

Мені лише потрібно було, using (System.Threading.ExecutionContext.SuppressFlow())і питання було вирішено для мене!
ZX9

10

У .NET Core мені вдалося отримати System.Net.Http.HttpClientз, UseDefaultCredentials = trueщоб перейти через автентифіковані облікові дані Windows до сервісу зворотного зв'язку, використовуючи 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

Це працювало для мене після того, як я налаштував користувача з доступом до Інтернету в сервісі Windows.

У моєму коді:

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

3

Гаразд, я взяв код Джошуна і зробив його загальним. Я не впевнений, чи повинен я реалізувати однотонний зразок для класу SynchronousPost. Можливо, хтось більш обізнаний може допомогти.

Впровадження

// Я припускаю, що у вас є власний конкретний тип. У моєму випадку я спочатку використовую код із класом, який називається FileCategory

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

Родовий клас тут. Ви можете передавати будь-який тип

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

Мої класи Api виглядають так, якщо вам цікаво

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

Я використовую ninject та repo pattern з одиницею роботи. У будь-якому випадку, загальний клас, що знаходиться вище, справді допомагає.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.