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


164

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

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


该Web应用程序使用以下命令向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/"));

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

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

我要使用的原因HttpClient是它具有一个与s兼容的异步API Task,而WebClient的asyc API需要通过事件进行处理。



看来HttpClient和WebClient认为不同的东西是DefaultCredentials。您是否尝试过HttpClient.setCredentials(...)?
Germann Arlington

顺便说一句,Web客户端具有DownloadStringTaskAsync在.NET 4.5,这也可以与异步/ AWAIT使用
LB

1
@GermannArlington:HttpClient没有SetCredentials()方法。你能指出我的意思吗?
adrianbanks'8

4
看来这已被修复(.net 4.5.1)?我尝试new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true }在由Windows身份验证的用户访问的Web服务器上创建,然后该网站对另一个远程资源进行了身份验证(没有设置标志就无法进行身份验证)。
GSerg 2015年

Answers:


67

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

我的解决方案使用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,这与WebAPI使用的JSON序列化程序相同。


1
最后我做了类似的事情,而且效果很好。异步问题不是问题,因为我希望阻止调用。
adrianbanks 2012年

136

您可以配置HttpClient为自动传递凭据,如下所示:

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

11
我知道该怎么做。该行为不是我想要的(如问题中所述)-“这向Windows服务发出了请求,但没有正确传递凭据(该服务将用户报告为IIS APPPOOL \ ASP.NET 4.0)。这不是我想要发生的事。”
adrianbanks

4
这似乎解决了我的问题,即iis仅启用了Windows身份验证。如果您只需要传递一些合法的凭证,就应该这样做。
Timmerz 2014年

不确定在模拟/委派方案中这是否与WebClient相同。在上述解决方案中使用HttpClient时,出现“目标主体名称不正确”的信息,但是使用具有类似设置的WebClient可以通过用户的凭据。
Peder Rice

这确实为我工作,并且日志显示正确的用户。虽然在图中为双跳,但我没想到它可以与NTLM作为基础身份验证方案一起使用,但是它可以工作。
Nitin Rastogi

使用最新版本的aspnet core怎么做?(2.2)。如果有人知道...
Nico

26

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

有关Windows身份验证选项以及它们如何工作的更多信息,请访问:http : //msdn.microsoft.com/en-us/library/ff647076.aspx


3
NTLM会将身份转发到下一台服务器,它无法执行此操作 ”-使用时WebClient为什么会这样做?这是我不了解的事情-如果不可能,它怎么做到的?
adrianbanks 2012年

2
使用Web客户端时,客户端和服务器之间仍然只有一个连接。它可以模拟该服务器上的用户(1跳),但不能将这些凭据转发到另一台计算机上(2跳-客户端到服务器到第二台服务器)。为此,您需要委派。
BlackSpy 2012年

1
完成尝试方式的唯一方法是让用户在ASP.NET应用程序的自定义对话框中键入其用户名和密码,将其存储为字符串,然后使用当您连接到Web API项目时,它们会设置您的身份。否则,您需要删除NTLM并转到Kerberos,以便可以将Kerboros票证传递到Web API项目。我强烈建议您阅读原始答案中附带的链接。开始之前,您想做的事需要对Windows身份验证有深刻的理解。
BlackSpy 2012年

2
@BlackSpy:我在Windows身份验证方面有丰富的经验。我想了解的是为什么WebClient可以传递NTLM凭据,但是不能传递HttpClient。我可以单独使用ASP.Net模拟来实现此目的,而不必使用Kerberos或存储用户名/密码。但是,这适用于WebClient
adrianbanks 2012年

1
如果不以文本形式传递用户名和密码,则不可能假冒超过1个跃点。它违反了假冒的规则,NTLM将不允许这样做。WebClient允许您跳1跳,因为您传递了凭据并以该用户身份在框中运行。如果查看安全日志,则会看到登录名-用户登录到系统。然后,除非您已将凭据作为文本传递并使用另一个Webclient实例登录到下一个框,否则您将无法从该计算机上以该用户身份运行。
BlackSpy 2012年

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()了我们的模拟身份。由于我们现在正在抑制流量,因此正在解决这一问题。由于使用/处置,在调用后将其重置。

seems,现在看来对我们有用!


2
非常感谢您进行此分析。这也解决了我的问题。现在,我的身份已正确传递到另一个Web应用程序!您节省了我几个小时的工作!我感到惊讶的是它的滴答数还不高。
justdan23 '18年

我只需要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服务中设置可以访问Internet的用户后,它对我有用。

在我的代码中:

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

3

好的,所以我采用了Joshoun的代码并使之通用。我不确定是否应该在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模式。无论如何,上面的泛型类确实有帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.