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

问题描述 投票:0回答:10

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

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


Web 应用程序使用

HttpClient
:

向 Windows 服务发出请求
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
的原因是它有一个与
Task
配合良好的异步 API,而
WebClient
的异步 API 需要通过事件来处理。

c# asp.net-web-api impersonation windows-security
10个回答
188
投票

您可以配置

HttpClient
自动传递凭据,如下所示:

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

76
投票

我也遇到了同样的问题。感谢 @tpeczek 在以下 SO 文章中所做的研究,我开发了一个同步解决方案:Unable to verify to ASP.NET Web Api service with 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,这与 WebAPI 使用的 JSON 序列化器相同。


32
投票

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

有关可用的 Windows 身份验证选项及其工作原理的更多信息,请从以下位置开始: http://msdn.microsoft.com/en-us/library/ff647076.aspx


24
投票

好的,感谢上面所有的贡献者。我正在使用 .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()
,这是我们的模拟身份。这是因为我们现在正在抑制流量。由于使用/处置,它在调用后会重置。

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


15
投票

在 .NET Core 中,我设法获得了

System.Net.Http.HttpClient
UseDefaultCredentials = true
,以使用
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);
    });
}

5
投票

我在 Windows 服务中设置了可以访问互联网的用户后,它对我有用。

在我的代码中:

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 模式。不管怎样,上面的通用类确实很有帮助。


1
投票

在 web.config 中将

identity
impersonation
设置为
true
并将
validateIntegratedModeConfiguration
设置为
false

<configuration>
  <system.web>
    <authentication mode="Windows" />
    <authorization>
      <deny users="?" />
    </authorization>
    <identity impersonate="true"/>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" ></validation>
  </system.webServer>
</configuration>

0
投票

根据@Sean的解决方案,我想出了这个。对于某些 mods,这也可以用于 GET,返回的字符串可以是 JsonSerializer.Deserialize(d) 到对象或对象列表。

public static string PostJsonString(string server, 
                                    string method, 
                                    HttpContent httpContent)
{
    string retval = string.Empty;
    string uri = server + method;

    try
    {
        // NOTE: the new HttpClientHandler() { UseDefaultCredentials = true } as the ctor
        //       parameter allows the existing user credentials to be used to make
        //       the HttpClient call.
        using (var httpClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true }))
        {
            using (var response = httpClient.PostAsync(uri, httpContent))
            {
                response.Wait();

                var result = response.Result;
                var readTask = result.Content.ReadAsStringAsync();
                readTask.Wait();

                retval = readTask.Result;

            }
        }

    }
    catch
    {
        throw;
    }

    return retval;
}

-3
投票
string url = "https://www..com";
System.Windows.Forms.WebBrowser webBrowser = new System.Windows.Forms.WebBrowser();
this.Controls.Add(webBrowser);

webBrowser.ScriptErrorsSuppressed = true;
webBrowser.Navigate(new Uri(url));

var webRequest = WebRequest.Create(url);
webRequest.Headers["Authorization"] = "Basic" + Convert.ToBase64String(Encoding.Default.GetBytes(Program.username + ";" + Program.password));
          
webRequest.Method = "POST";
        
© www.soinside.com 2019 - 2024. All rights reserved.