Pour l'authentification de base, j'ai implémenté un HttpMessageHandler
personnalisé basé sur l'exemple montré dans la réponse de Darin Dimitrov ici: https://stackoverflow.com/a/11536349/270591
Le code crée une instance principal
de type GenericPrincipal
avec le nom d'utilisateur et les rôles, puis définit ce principal sur le principal actuel du thread:
Thread.CurrentPrincipal = principal;
Plus loin dans une méthode ApiController
, le principal peut être lu en accédant à la propriété User
des contrôleurs:
public class ValuesController : ApiController
{
public void Post(TestModel model)
{
var user = User; // this should be the principal set in the handler
//...
}
}
Cela semblait bien fonctionner jusqu'à ce que j'ajoute récemment un MediaTypeFormatter
personnalisé qui utilise la bibliothèque Task
comme ceci:
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream,
HttpContent content, IFormatterLogger formatterLogger)
{
var task = Task.Factory.StartNew(() =>
{
// some formatting happens and finally a TestModel is returned,
// simulated here by just an empty model
return (object)new TestModel();
});
return task;
}
(J'ai cette approche pour démarrer une tâche avec Task.Factory.StartNew
Dans ReadFromStreamAsync
à partir d'un exemple de code. Est-ce mal et peut-être la seule raison du problème?)
Maintenant, "parfois" - et pour moi, il semble être aléatoire - le principal User
dans la méthode du contrôleur n'est plus le principal que j'ai défini dans le MessageHandler, c'est-à-dire le nom d'utilisateur, Authenticated
le drapeau et les rôles sont tous perdus. La raison semble être que le MediaTypeFormatter personnalisé provoque un changement de thread entre MessageHandler et la méthode du contrôleur. J'ai confirmé cela en comparant les valeurs de Thread.CurrentThread.ManagedThreadId
Dans le MessageHandler et dans la méthode du contrôleur. "Parfois", ils sont différents, puis le principal est "perdu".
J'ai maintenant cherché une alternative à la configuration de Thread.CurrentPrincipal
Pour transférer le principal en toute sécurité du MessageHandler personnalisé vers la méthode du contrôleur et dans ce billet de blog les propriétés de la requête sont utilisées:
request.Properties.Add(HttpPropertyKeys.UserPrincipalKey,
new GenericPrincipal(identity, new string[0]));
Je voulais tester cela, mais il semble que la classe HttpPropertyKeys
(qui se trouve dans l'espace de noms System.Web.Http.Hosting
) N'a plus de propriété UserPrincipalKey
dans les dernières versions de WebApi (release candidate ainsi que la version finale de la semaine dernière).
Ma question est la suivante: comment puis-je modifier le dernier extrait de code ci-dessus pour qu'il fonctionne avec la version WebAPI actuelle? Ou généralement: comment puis-je définir le principal d'utilisateur dans un MessageHandler personnalisé et y accéder de manière fiable dans une méthode de contrôleur?
Modifier
Il est mentionné ici que "HttpPropertyKeys.UserPrincipalKey
... se résout en “MS_UserPrincipal”
", J'ai donc essayé d'utiliser:
request.Properties.Add("MS_UserPrincipal",
new GenericPrincipal(identity, new string[0]));
Mais cela ne fonctionne pas comme prévu: la propriété ApiController.User
Ne contient pas le principal ajouté à la collection Properties
ci-dessus.
Le problème de la perte du principal sur un nouveau thread est mentionné ici:
http://leastprivilege.com/2012/06/25/important-setting-the-client-principal-in-asp-net-web-api/
Important: définition du client principal dans l'API Web ASP.NET
En raison de certains mécanismes malheureux enfouis profondément dans ASP.NET, la définition de Thread.CurrentPrincipal dans l'hébergement Web d'API Web n'est pas suffisante.
Lors de l'hébergement dans ASP.NET, Thread.CurrentPrincipal peut être remplacé par HttpContext.Current.User lors de la création de nouveaux threads. Cela signifie que vous devez définir le principal à la fois sur le thread et le contexte HTTP.
Et ici: http://aspnetwebstack.codeplex.com/workitem/264
Aujourd'hui, vous devrez définir les deux éléments suivants pour l'utilisateur principal si vous utilisez un gestionnaire de messages personnalisé pour effectuer l'authentification dans le scénario hébergé sur le Web.
IPrincipal principal = new GenericPrincipal( new GenericIdentity("myuser"), new string[] { "myrole" }); Thread.CurrentPrincipal = principal; HttpContext.Current.User = principal;
J'ai ajouté la dernière ligne HttpContext.Current.User = principal
(Besoins using System.Web;
) au gestionnaire de messages et la propriété User
dans le ApiController
a toujours le principal correct maintenant, même si le thread a changé en raison de la tâche dans MediaTypeFormatter.
Modifier
Juste pour le souligner: la définition du principal de l'utilisateur actuel de HttpContext
n'est nécessaire que lorsque WebApi est hébergé dans ASP.NET/IIS. Pour l'auto-hébergement, ce n'est pas nécessaire (et pas possible car HttpContext
est une construction ASP.NET et n'existe pas lorsqu'il est auto-hébergé).
En utilisant votre MessageHandler personnalisé, vous pouvez ajouter le MS_UserPrincipal
propriété en appelant le HttpRequestMessageExtensionMethods.SetUserPrincipal
méthode d'extension définie dans System.ServiceModel.Channels
:
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var user = new GenericPrincipal(new GenericIdentity("UserID"), null);
request.SetUserPrincipal(user);
return base.SendAsync(request, cancellationToken);
}
Notez que cela n'ajoute que cette propriété à la collection Propriétés de la demande, cela ne change pas l'utilisateur attaché à ApiController.
Pour éviter le changement de contexte, essayez d'utiliser un TaskCompletionSource<object>
au lieu de démarrer manuellement une autre tâche dans votre MediaTypeFormatter
personnalisé:
public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
var tcs = new TaskCompletionSource<object>();
// some formatting happens and finally a TestModel is returned,
// simulated here by just an empty model
var testModel = new TestModel();
tcs.SetResult(testModel);
return tcs.Task;
}