Как заставить 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);
});
}