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

我有一个 Web 应用程序(托管在 IIS 中)与 Windows 服务对话。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的一个 bug) ?

我之所以要使用 HttpClient,是因为它有一个与 Task协作良好的异步 API,而 WebClient的 asyc API 需要用事件来处理。

279253 次浏览

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

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

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

我的解决方案使用 WebClient,正如您正确指出的那样,它可以毫无问题地传递凭据。HttpClient无法工作的原因是 Windows 安全性禁用了在模拟帐户下创建新线程的能力(参见上面的 SO 文章)HttpClient通过 Task Factory 创建新线程,从而导致错误。另一方面,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 序列化程序相同。

好的,所以我采用了 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();
}
}

我使用的是带有工作单元的注入和 repo 模式。

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

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

好的,感谢上面所有的贡献者。我在吸毒。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(),这是我们的模拟身份。因为我们现在正在抑制电流,所以这个被发现了。由于使用/释放,这将在调用后重置。

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

当我在 Windows 服务中设置了一个可以上网的用户后,这个方法对我很有效。

在我的代码里:

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

进去。NET 核心,我设法得到一个 System.Net.Http.HttpClientUseDefaultCredentials = 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);
});
}

在 web.config 中将 identityimpersonation设置为 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>
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";