【问题标题】:How to get HttpClient to pass credentials along with the request?如何让 HttpClient 与请求一起传递凭据?
【发布时间】:2012-08-26 01:56:39
【问题描述】:

我有一个与 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 的原因是它有一个异步API,可以很好地与Tasks 配合使用,而WebClient 的异步API 需要处理事件。子>

【问题讨论】:

  • 似乎 HttpClient 和 WebClient 认为不同的东西是 DefaultCredentials。你试过 HttpClient.setCredentials(...) 吗?
  • 顺便说一句,WebClient在.Net 4.5中有DownloadStringTaskAsync,也可以和async/await一起使用
  • @GermannArlington: HttpClient 没有 SetCredentials() 方法。你能指出我的意思吗?
  • 看来这已被修复(.net 4.5.1)?我尝试在经过 Windows 身份验证的用户访问的 Web 服务器上创建 new HttpClient(new HttpClientHandler() { AllowAutoRedirect = true, UseDefaultCredentials = true },然后该网站确实对另一个远程资源进行了身份验证(如果没有设置标志就不会进行身份验证)。

标签: c# asp.net-web-api impersonation windows-security


【解决方案1】:

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

有关您可用的 Windows 身份验证选项及其工作方式的更多信息,请访问: http://msdn.microsoft.com/en-us/library/ff647076.aspx

【讨论】:

  • "NTLM 将身份转发到下一个服务器,这是它无法做到的" - 使用WebClient 时为什么会这样做?这是我不明白的事情——如果不可能,它是怎么做到的?
  • 当使用网络客户端时,客户端和服务器之间仍然只有一个连接。它可以模拟该服务器上的用户(1 个跃点),但不能将这些凭据转发到另一台机器(2 个跃点 - 客户端到服务器到第二个服务器)。为此,您需要委托。
  • 以您尝试执行的方式完成您尝试执行的操作的唯一方法是让用户在您的 ASP.NET 应用程序上的自定义对话框中键入他的用户名和密码,将它们存储为字符串,然后在连接到 Web API 项目时使用它们来设置您的身份。否则,您需要删除 NTLM 并移至 Kerberos,以便您可以将 Kerboros 票证传递给 Web API 项目。我强烈建议阅读我在原始答案中附加的链接。您尝试做的事情需要在开始之前对 Windows 身份验证有深入的了解。
  • @BlackSpy:我在 Windows 身份验证方面有丰富的经验。我想了解的是为什么 WebClient 可以传递 NTLM 凭据,但 HttpClient 不能。我可以单独使用 ASP.Net 模拟来实现这一点,而不必使用 Kerberos 或存储用户名/密码。但是,这仅适用于WebClient。
  • 如果不将用户名和密码作为文本传递,应该不可能模拟超过 1 个跃点。它违反了模拟规则,NTLM 不允许这样做。 WebClient 允许您跳过 1 跳,因为您传递了凭据并在框上以该用户身份运行。如果您查看安全日志,您将看到登录 - 用户登录到系统。除非您已将凭据作为文本传递并使用另一个 Web 客户端实例登录到下一个框,否则您将无法从该计算机以该用户身份运行。
【解决方案2】:

我也遇到了同样的问题。由于@tpeczek 在以下 SO 文章中所做的研究,我开发了一个同步解决方案:Unable to authenticate 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 序列化程序相同。

【讨论】:

  • 我最后做了类似的事情,而且效果很好。异步问题不是问题,因为我希望阻止调用。
  • WebClient 与 HttpClient 有很大不同,最终 WebClient 受到更多限制(例如,如果您使用 UploadValues 进行 POST,则无法获得 Stream 响应)。如果您已经在使用或需要使用 HttpClient,则基于 HttpClient 的解决方案会更好。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

    • 我知道该怎么做。该行为不是我想要的(如问题中所述)-“这向 Windows 服务发出请求,但未正确传递凭据(该服务将用户报告为 IIS APPPOOL\ASP.NET 4.0)。这这不是我想要发生的。”
    • 这似乎解决了我的问题,即 iis 仅启用了 Windows 身份验证。如果您只需要传递一些合法凭据,就应该这样做。
    • 不确定这是否与 WebClient 在模拟/委托场景中的工作方式相同。将 HttpClient 与上述解决方案一起使用时,我得到“目标主体名称不正确”,但使用具有类似设置的 WebClient 会传递用户的凭据。
    • 这对我有用,并且日志显示正确的用户。虽然在图片中使用双跳,我没想到它会使用 NTLM 作为底层身份验证方案,但它确实有效。
    • 为什么这与 System.Net.CredentialCache.DefaultCredentials 或 System.Net.CredentialCache.DefaultNetworkCredentials 的工作方式不同?
    【解决方案5】:

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

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

    【讨论】:

    • 非常感谢您进行此分析。这也解决了我的情况。现在我的身份已正确传递给其他 Web 应用程序!你为我节省了几个小时的工作时间!我很惊讶它的滴答计数没有更高。
    • 我只需要using (System.Threading.ExecutionContext.SuppressFlow()),问题就为我解决了!
    【解决方案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。

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

      【讨论】:

        【解决方案8】:

        在 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>
        

        【讨论】:

        • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center。
        猜你喜欢
        • 2018-01-28
        • 1970-01-01
        • 2017-06-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多