J'ai une application Web (hébergée dans IIS) qui communique avec un service Windows. Le service Windows utilise l'API Web ASP.Net MVC (auto-hébergé) et peut donc être communiqué via http à l'aide de JSON. L'application Web est configurée pour effectuer l'emprunt d'identité, l'idée étant que l'utilisateur qui fait la demande à l'application Web devrait être l'utilisateur que l'application Web utilise pour faire la demande au service. La structure ressemble à ceci:
(L'utilisateur surligné en rouge est l'utilisateur auquel il est fait référence dans les exemples ci-dessous.)
L'application Web envoie des demandes au service Windows à l'aide de HttpClient
:
var httpClient = new HttpClient(new HttpClientHandler()
{
UseDefaultCredentials = true
});
httpClient.GetStringAsync("http://localhost/some/endpoint/");
La demande est envoyée au service Windows, mais les informations d'identification ne sont pas transmises correctement (le service signale l'utilisateur sous la forme IIS APPPOOL\ASP.NET 4.0
). Ce n'est pas ce que je veux arriver .
Si je modifie le code ci-dessus pour utiliser un WebClient
, les informations d'identification de l'utilisateur sont correctement transmises:
WebClient c = new WebClient
{
UseDefaultCredentials = true
};
c.DownloadStringAsync(new Uri("http://localhost/some/endpoint/"));
Avec le code ci-dessus, le service signale l'utilisateur comme l'utilisateur qui a effectué la demande auprès de l'application Web.
Qu'est-ce que je fais de mal avec l'implémentation HttpClient
qui l'empêche de transmettre correctement les informations d'identification (ou s'agit-il d'un bogue avec HttpClient
)?
La raison pour laquelle je souhaite utiliser HttpClient
est que son API asynchrone fonctionne bien avec Task
s, alors que l'API asyc de WebClient
doit être gérée avec des événements.
J'avais aussi ce même problème. J'ai développé une solution synchrone grâce aux recherches effectuées par @tpeczek dans l'article SO suivant: Impossible de s'authentifier auprès du service ASP.NET Web Api avec HttpClient
Ma solution utilise un WebClient
, qui, comme vous l'avez indiqué correctement, transmet les informations d'identification sans problème. La raison pour laquelle HttpClient
ne fonctionne pas, c'est que la sécurité de Windows a désactivé la possibilité de créer de nouveaux threads sous un compte emprunté (voir l'article SO ci-dessus.) HttpClient
crée de nouveaux threads via le Task Factory provoquant ainsi l'erreur. WebClient
en revanche, s'exécute de manière synchrone sur le même thread en contournant ainsi la règle et en transmettant ses informations d'identification.
Bien que le code fonctionne, l'inconvénient est qu'il ne fonctionnera pas de manière asynchrone.
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();
}
Remarque: Nécessite le package NuGet: Newtonsoft.Json, identique au sérialiseur JSON utilisé par WebAPI.
Vous pouvez configurer HttpClient
pour qu'il passe automatiquement les informations d'identification comme ceci:
myClient = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true })
Ce que vous essayez de faire est de faire en sorte que NTLM transmette l’identité au prochain serveur, ce qu’il ne peut pas faire. Il ne peut effectuer qu’une usurpation d’identité qui ne vous donne accès qu’aux ressources locales. Cela ne vous laissera pas dépasser une machine. L'authentification Kerberos prend en charge la délégation (ce dont vous avez besoin) à l'aide de tickets. Le ticket peut être transféré lorsque tous les serveurs et applications de la chaîne sont correctement configurés et que Kerberos est configuré correctement sur le domaine. En bref, vous devez passer de NTLM à Kerberos.
Pour plus d'informations sur les options d'authentification Windows disponibles et sur leur fonctionnement, commencez à l'adresse suivante: http://msdn.Microsoft.com/en-us/library/ff647076.aspx
OK, merci à tous les contributeurs ci-dessus. J'utilise .NET 4.6 et nous avons également eu le même problème. J'ai passé du temps à déboguer System.Net.Http
, en particulier le HttpClientHandler
, et j'ai trouvé ce qui suit:
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);
}
Ainsi, après avoir évalué que la ExecutionContext.IsFlowSuppressed()
était peut-être le coupable, j'ai enveloppé notre code d'emprunt d'identité comme suit:
using (((WindowsIdentity)ExecutionContext.Current.Identity).Impersonate())
using (System.Threading.ExecutionContext.SuppressFlow())
{
// HttpClient code goes here!
}
Le code à l'intérieur de SafeCaptureIdenity
(pas ma faute d'orthographe), saisit WindowsIdentity.Current()
qui est notre identité imitée. Ceci est pris en compte parce que nous supprimons maintenant le flux. En raison de l'utilisation/dispose, il est réinitialisé après l'appel.
Cela semble maintenant fonctionner pour nous, ouf!
Dans .NET Core, j'ai réussi à obtenir un System.Net.Http.HttpClient
avec UseDefaultCredentials = true
à transmettre aux informations d'identification Windows de l'utilisateur authentifié à un service dorsal à l'aide de 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);
});
}
Cela a fonctionné pour moi après avoir configuré un utilisateur avec un accès Internet dans le service Windows.
Dans mon code:
HttpClientHandler handler = new HttpClientHandler();
handler.Proxy = System.Net.WebRequest.DefaultWebProxy;
handler.Proxy.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
.....
HttpClient httpClient = new HttpClient(handler)
....
Ok, j'ai pris le code Joshoun et l'ai rendu générique. Je ne suis pas sûr de devoir implémenter un modèle singleton sur la classe SynchronousPost. Peut-être que quelqu'un de plus intelligent peut aider.
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();
}
}
J'utilise ninject, et repo pattern avec unité de travail. Quoi qu'il en soit, la classe générique ci-dessus aide vraiment.