我有一个与 Windows 服务通信的 Web 应用程序(托管在 IIS 中)。 Windows 服务使用 ASP.Net MVC Web API(自托管),因此可以使用 JSON 通过 http 进行通信。 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 需要通过事件来处理。
您可以配置
HttpClient
自动传递凭据,如下所示:
var myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
我也遇到了同样的问题。感谢 @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 序列化器相同。
您想要做的是让 NTLM 将身份转发到下一个服务器,但它无法做到这一点 - 它只能进行模拟,这只能让您访问本地资源。它不会让你跨越机器边界。 Kerberos 身份验证通过使用票证支持委派(您需要的),并且当链中的所有服务器和应用程序都正确配置并且 Kerberos 在域上正确设置时,可以转发票证。 因此,简而言之,您需要从使用 NTLM 切换到 Kerberos。
有关可用的 Windows 身份验证选项及其工作原理的更多信息,请从以下位置开始: http://msdn.microsoft.com/en-us/library/ff647076.aspx
好的,感谢上面所有的贡献者。我正在使用 .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()
,这是我们的模拟身份。这是因为我们现在正在抑制流量。由于使用/处置,它在调用后会重置。
现在看来对我们有用,唷!
在 .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);
});
}
我在 Windows 服务中设置了可以访问互联网的用户后,它对我有用。
在我的代码中:
HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = System.Net.WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
.....
HttpClient httpClient = new HttpClient(handler)
....
好吧,我采用了 Joshoun 代码并将其变得通用。我不确定是否应该在 SynchronousPost 类上实现单例模式。也许更有知识的人可以提供帮助。
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; }
}
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 模式。不管怎样,上面的通用类确实很有帮助。
在 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>
根据@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;
}
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";