Как заставить 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, тогда как WebClientасинхронный API s должен обрабатываться с событиями.

adrianbanks
источник
Возможный дубликат stackoverflow.com/q/10308938/1045728
Томми Гровнес
Похоже, что HttpClient и WebClient считают разные вещи DefaultCredentials. Вы пробовали HttpClient.setCredentials (...)?
Герман Арлингтон
Кстати, у WebClient есть DownloadStringTaskAsync.Net 4.5, который также можно использовать с async / await
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 service с помощью HttpClient

Мое решение использует 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. если вам просто нужно пройти некоторые законные учетные данные, это следует сделать.
Timmerz
Не уверен, что это работает так же, как WebClient в сценариях олицетворения / делегирования. Я получаю «Целевое имя субъекта неверно» при использовании HttpClient с вышеупомянутым решением, но использование WebClient с аналогичной настройкой пропускает учетные данные пользователя.
Педер Райс
Это сработало для меня, и логи показывают правильного пользователя. Хотя, с двойным переходом на рисунке, я не ожидал, что он будет работать с NTLM в качестве базовой схемы аутентификации, но он работает.
Нитин Растоги
Как сделать то же самое, используя последнюю версию ядра aspnet? (2.2). Если кто-нибудь знает ...
Нико
26

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

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

BlackSpy
источник
3
« NTLM для пересылки удостоверения на следующий сервер, что он не может сделать » - почему он это делает при использовании WebClient? Это то, чего я не понимаю - если это невозможно, то почему это происходит ?
Adrianbanks
2
При использовании веб-клиента между клиентом и сервером остается только одно соединение. Он может выдавать себя за пользователя на этом сервере (1 прыжок), но не может пересылать эти учетные данные на другой компьютер (2 прыжка - с клиента на сервер на 2-й сервер). Для этого вам нужно делегирование.
BlackSpy
1
Единственный способ выполнить то, что вы пытаетесь сделать, - это заставить пользователя ввести свое имя пользователя и пароль в настраиваемое диалоговое окно в приложении ASP.NET, сохранить их в виде строк и затем использовать их, чтобы установить вашу личность при подключении к вашему проекту веб-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()который захватывает нашу личность. Это поднято, потому что мы теперь подавляем поток. Из-за использования / утилизации это сбрасывается после вызова.

Кажется, теперь это работает для нас, фу!

Chullybun
источник
2
Большое вам спасибо за этот анализ. Это исправило мою ситуацию тоже. Теперь моя личность правильно передается другому веб-приложению! Вы сэкономили мне часы работы! Я удивлен, что это не выше по счету тиков.
justdan23
Я только нуждался, using (System.Threading.ExecutionContext.SuppressFlow())и проблема была решена для меня!
ZX9
10

В .NET Ядра, мне удалось получить 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);
    });
}
scourge192
источник
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 и репо с единицей работы. В любом случае, общий класс выше действительно помогает.

скрытый
источник