Как заставить HttpClient передавать учетные данные вместе с запросом?

У меня есть веб-приложение (размещенное в IIS), которое ведет переговоры с службой Windows. Служба Windows использует ASP.Net MVC Web API (самообслуживание) и поэтому может передаваться через 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, который хорошо работает с Task s, тогда как API-интерфейс WebClient asyc должен обрабатываться событиями.

Ответ 1

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

В моем решении используется WebClient, который, как вы правильно отметили, передает учетные данные без проблем. Причина HttpClient не работает из-за того, что Windows-защита отключает возможность создания новых потоков под олицетворенной учетной записью (см. Статью SO выше). HttpClient создает новые потоки через Task Factory, вызывая таким образом ошибку. WebClient, с другой стороны, выполняется синхронно в одном потоке, тем самым минуя правило и пересылая свои учетные данные.

Несмотря на то, что код работает, недостатком является то, что он не будет работать async.

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, использует.

Ответ 2

Вы можете настроить HttpClient для автоматической передачи учетных данных следующим образом:

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

Ответ 3

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

Подробнее о доступных для Windows проверках подлинности и способах их работы: http://msdn.microsoft.com/en-us/library/ff647076.aspx

Ответ 4

Хорошо, так что спасибо всем авторам выше. Я использую .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() которая является нашей олицетворенной личностью. Это поднято, потому что мы теперь подавляем поток. Из-за использования/утилизации это сбрасывается после вызова.

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

Ответ 5

Хорошо, поэтому я взял код Joshoun и сделал его общим. Я не уверен, что я должен реализовать singleton-шаблон в классе 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 и шаблон репо с единицей работы. В любом случае, общий класс выше действительно помогает.

Ответ 6

Он работал у меня после того, как я установил пользователя с доступом в Интернет в службе Windows.

В моем коде:

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

Ответ 7

В .NET Core мне удалось получить System.Net.Http.HttpClient с UseDefaultCredentials = true чтобы передать учетные данные Windows, прошедшие проверку подлинности, в WindowsIdentity.RunImpersonated службу с помощью 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);
    });
}