如何让HttpClient与请求一起传递凭据?

时间:2020-12-20 00:00:02

I have a web application (hosted in IIS) that talks to a Windows service. The Windows service is using the ASP.Net MVC Web API (self-hosted), and so can be communicated with over http using JSON. The web application is configured to do impersonation, the idea being that the user who makes the request to the web application should be the user that the web application uses to make the request to the service. The structure looks like this:

我有一个与Windows服务对话的Web应用程序(在IIS中托管)。 Windows服务使用ASP.Net MVC Web API(自托管),因此可以使用JSON通过http进行通信。 Web应用程序配置为进行模拟,其想法是向Web应用程序发出请求的用户应该是Web应用程序用来向服务发出请求的用户。结构如下所示:

如何让HttpClient与请求一起传递凭据?

(The user highlighted in red is the user being referred to in the examples below.)

(以红色突出显示的用户是以下示例中引用的用户。)


The web application makes requests to the Windows service using an HttpClient:

Web应用程序使用HttpClient向Windows服务发出请求:

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

This makes the request to the Windows service, but does not pass the credentials over correctly (the service reports the user as IIS APPPOOL\ASP.NET 4.0). This is not what I want to happen.

这会向Windows服务发出请求,但不会正确传递凭据(该服务将用户报告为IIS APPPOOL \ ASP.NET 4.0)。这不是我想要发生的事情。

If I change the above code to use a WebClient instead, the credentials of the user are passed correctly:

如果我将上述代码更改为使用WebClient,则会正确传递用户的凭据:

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

With the above code, the service reports the user as the user who made the request to the web application.

使用上述代码,服务将用户报告为向Web应用程序发出请求的用户。

What am I doing wrong with the HttpClient implementation that is causing it to not pass the credentials correctly (or is it a bug with the HttpClient)?

HttpClient实现导致它无法正确传递凭据(或者是HttpClient的错误),我做错了什么?

The reason I want to use the HttpClient is that it has an async API that works well with Tasks, whereas the WebClient's asyc API needs to be handled with events.

我想使用HttpClient的原因是它有一个异步API,可以很好地处理任务,而WebClient的asyc API需要处理事件。

7 个解决方案

#1


I was also having this same problem. I developed a synchronous solution thanks to the research done by @tpeczek in the following SO article: Unable to authenticate to ASP.NET Web Api service with HttpClient

我也有同样的问题。由于@tpeczek在以下SO文章中所做的研究,我开发了一个同步解决方案:无法使用HttpClient对ASP.NET Web Api服务进行身份验证

My solution uses a WebClient, which as you correctly noted passes the credentials without issue. The reason HttpClient doesn't work is because of Windows security disabling the ability to create new threads under an impersonated account (see SO article above.) HttpClient creates new threads via the Task Factory thus causing the error. WebClient on the other hand, runs synchronously on the same thread thereby bypassing the rule and forwarding its credentials.

我的解决方案使用WebClient,正如您正确指出的那样,它会毫无问题地传递凭据。 HttpClient不起作用的原因是因为Windows安全性禁用了在模拟帐户下创建新线程的能力(参见上面的SO文章。)HttpClient通过任务工厂创建新线程,从而导致错误。另一方面,WebClient在同一个线程上同步运行,从而绕过规则并转发其凭据。

Although the code works, the downside is that it will not work 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();}

Note: Requires NuGet package: Newtonsoft.Json, which is the same JSON serializer WebAPI uses.

注意:需要NuGet包:Newtonsoft.Json,它与WebAPI使用的JSON序列化程序相同。

#2


You can configure HttpClient to automatically pass credentials like this:

您可以将HttpClient配置为自动传递以下凭据:

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

#3


What you are trying to do is get NTLM to forward the identity on to the next server, which it cannot do - it can only do impersonation which only gives you access to local resources. It won't let you cross a machine boundary. Kerberos authentication supports delegation (what you need) by using tickets, and the ticket can be forwarded on when all servers and applications in the chain are correctly configured and Kerberos is set up correctly on the domain. So, in short you need to switch from using NTLM to Kerberos.

你要做的是让NTLM将身份转发到下一个服务器,这是它无法做到的 - 它只能进行模拟,只能让你访问本地资源。它不会让你跨越机器边界。 Kerberos身份验证通过使用故障单支持委派(您需要),并且当链中的所有服务器和应用程序都已正确配置并且在域上正确设置Kerberos时,可以转发故障单。因此,简而言之,您需要从使用NTLM切换到Kerberos。

For more on Windows Authentication options available to you and how they work start at:http://msdn.microsoft.com/en-us/library/ff647076.aspx

有关Windows可用的身份验证选项以及它们的工作原理,请访问:http://msdn.microsoft.com/en-us/library/ff647076.aspx

#4


OK, so thanks to all of the contributors above. I am using .NET 4.6 and we also had the same issue. I spent time debugging System.Net.Http, specifically the HttpClientHandler, and found the following:

好的,感谢上面的所有贡献者。我使用的是.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);    }

So after assessing that the ExecutionContext.IsFlowSuppressed() might have been the culprit, I wrapped our Impersonation code as follows:

因此,在评估ExecutionContext.IsFlowSuppressed()可能是罪魁祸首后,我将我们的模拟代码包装如下:

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

The code inside of SafeCaptureIdenity (not my spelling mistake), grabs WindowsIdentity.Current() which is our impersonated identity. This is being picked up because we are now suppressing flow. Because of the using/dispose this is reset after invocation.

SafeCaptureIdenity中的代码(不是我的拼写错误),抓住了我们冒充身份的WindowsIdentity.Current()。这是因为我们现在正在抑制流动。由于使用/ dispose,这在调用后重置。

It now seems to work for us, phew!

它现在似乎对我们有用,p!

#5


In .NET Core, I managed to get a System.Net.Http.HttpClient with UseDefaultCredentials = true to pass through the authenticated user's Windows credentials to a back end service by using WindowsIdentity.RunImpersonated.

在.NET Core中,我设法使用UseDefaultCredentials = true获取System.Net.Http.HttpClient,以使用WindowsIdentity.RunImpersonated将经过身份验证的用户的Windows凭据传递给后端服务。

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

#6


Ok so I took Joshoun code and made it generic. I am not sure if I should implement singleton pattern on SynchronousPost class. Maybe someone more knowledgeble can help.

好的,所以我拿了Joshoun代码并使它成为通用的。我不确定我是否应该在SynchronousPost类上实现单例模式。也许更有知识的人可以提供帮助。

Implementation

//I assume you have your own concrete type. In my case I have am using code first with a class called FileCategory

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

Generic Class here. You can pass any type

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

My Api classs looks like this, if you are curious

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

I am using ninject, and repo pattern with unit of work. Anyways, the generic class above really helps.

我正在使用ninject,以及工作单元的repo模式。无论如何,上面的通用类确实有帮助。

#7


It worked for me after I set up a user with internet access in the Windows service.

在Windows服务中设置具有Internet访问权限的用户后,它对我有用。

In my code:

在我的代码中:

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

#1


I was also having this same problem. I developed a synchronous solution thanks to the research done by @tpeczek in the following SO article: Unable to authenticate to ASP.NET Web Api service with HttpClient

我也有同样的问题。由于@tpeczek在以下SO文章中所做的研究,我开发了一个同步解决方案:无法使用HttpClient对ASP.NET Web Api服务进行身份验证

My solution uses a WebClient, which as you correctly noted passes the credentials without issue. The reason HttpClient doesn't work is because of Windows security disabling the ability to create new threads under an impersonated account (see SO article above.) HttpClient creates new threads via the Task Factory thus causing the error. WebClient on the other hand, runs synchronously on the same thread thereby bypassing the rule and forwarding its credentials.

我的解决方案使用WebClient,正如您正确指出的那样,它会毫无问题地传递凭据。 HttpClient不起作用的原因是因为Windows安全性禁用了在模拟帐户下创建新线程的能力(参见上面的SO文章。)HttpClient通过任务工厂创建新线程,从而导致错误。另一方面,WebClient在同一个线程上同步运行,从而绕过规则并转发其凭据。

Although the code works, the downside is that it will not work 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();}

Note: Requires NuGet package: Newtonsoft.Json, which is the same JSON serializer WebAPI uses.

注意:需要NuGet包:Newtonsoft.Json,它与WebAPI使用的JSON序列化程序相同。

#2


You can configure HttpClient to automatically pass credentials like this:

您可以将HttpClient配置为自动传递以下凭据:

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

#3


What you are trying to do is get NTLM to forward the identity on to the next server, which it cannot do - it can only do impersonation which only gives you access to local resources. It won't let you cross a machine boundary. Kerberos authentication supports delegation (what you need) by using tickets, and the ticket can be forwarded on when all servers and applications in the chain are correctly configured and Kerberos is set up correctly on the domain. So, in short you need to switch from using NTLM to Kerberos.

你要做的是让NTLM将身份转发到下一个服务器,这是它无法做到的 - 它只能进行模拟,只能让你访问本地资源。它不会让你跨越机器边界。 Kerberos身份验证通过使用故障单支持委派(您需要),并且当链中的所有服务器和应用程序都已正确配置并且在域上正确设置Kerberos时,可以转发故障单。因此,简而言之,您需要从使用NTLM切换到Kerberos。

For more on Windows Authentication options available to you and how they work start at:http://msdn.microsoft.com/en-us/library/ff647076.aspx

有关Windows可用的身份验证选项以及它们的工作原理,请访问:http://msdn.microsoft.com/en-us/library/ff647076.aspx

#4


OK, so thanks to all of the contributors above. I am using .NET 4.6 and we also had the same issue. I spent time debugging System.Net.Http, specifically the HttpClientHandler, and found the following:

好的,感谢上面的所有贡献者。我使用的是.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);    }

So after assessing that the ExecutionContext.IsFlowSuppressed() might have been the culprit, I wrapped our Impersonation code as follows:

因此,在评估ExecutionContext.IsFlowSuppressed()可能是罪魁祸首后,我将我们的模拟代码包装如下:

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

The code inside of SafeCaptureIdenity (not my spelling mistake), grabs WindowsIdentity.Current() which is our impersonated identity. This is being picked up because we are now suppressing flow. Because of the using/dispose this is reset after invocation.

SafeCaptureIdenity中的代码(不是我的拼写错误),抓住了我们冒充身份的WindowsIdentity.Current()。这是因为我们现在正在抑制流动。由于使用/ dispose,这在调用后重置。

It now seems to work for us, phew!

它现在似乎对我们有用,p!

#5


In .NET Core, I managed to get a System.Net.Http.HttpClient with UseDefaultCredentials = true to pass through the authenticated user's Windows credentials to a back end service by using WindowsIdentity.RunImpersonated.

在.NET Core中,我设法使用UseDefaultCredentials = true获取System.Net.Http.HttpClient,以使用WindowsIdentity.RunImpersonated将经过身份验证的用户的Windows凭据传递给后端服务。

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

#6


Ok so I took Joshoun code and made it generic. I am not sure if I should implement singleton pattern on SynchronousPost class. Maybe someone more knowledgeble can help.

好的,所以我拿了Joshoun代码并使它成为通用的。我不确定我是否应该在SynchronousPost类上实现单例模式。也许更有知识的人可以提供帮助。

Implementation

//I assume you have your own concrete type. In my case I have am using code first with a class called FileCategory

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

Generic Class here. You can pass any type

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

My Api classs looks like this, if you are curious

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

I am using ninject, and repo pattern with unit of work. Anyways, the generic class above really helps.

我正在使用ninject,以及工作单元的repo模式。无论如何,上面的通用类确实有帮助。

#7


It worked for me after I set up a user with internet access in the Windows service.

在Windows服务中设置具有Internet访问权限的用户后,它对我有用。

In my code:

在我的代码中:

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