Introduction
L’objectif de cet article est tout d’abord de démontrer comment monitorer les appels à un service WCF (en enregistrant tous les appels ainsi que leurs temps d’exécution et les utilisateurs). Par ailleurs nous verrons que cela peut s’appliquer également pour fournir une gestion de la sécurité sans dupliquer le code ou appeler des méthodes explicitement.
WCF offre la possibilité d’ajouter des comportements sur un service ou sur des méthodes d’un service. Nous allons donc créer nos propres comportements (le premier pour logguer, le second pour la sécurité), et les appliquer sur les méthodes de notre choix.
WCF Attributes Sources (Nécessite le framework .net 4.5 et Visual Studio 2013).
Exécuter Visual Studio en mode administrateur pour démarrer le service WCF (ou autorisez le port)
Création du service
Pour notre exemple nous allons définir un contrat d’interface « IMyCompanyService » composé de 2 méthodes tel que :
[ServiceContract] public interface IMyCompanyService { /// <summary> /// Provides User object according to the username/password /// </summary> /// <param name="UserName"></param> /// <param name="Password"></param> /// <returns>User object</returns> [OperationContract] User Login(string UserName, string Password); /// <summary> /// Returns a simple string /// </summary> /// <param name="value"></param> /// <param name="currentUserId">Logged user</param> /// <returns>String which contains the input parameter named value</returns> [OperationContract] [FaultContract(typeof(string))] string GetData(int value, Guid currentUserId); } |
Logique d’utilisation du service
Le client fait un premier appel à la méthode « Login », si l’authentification est correcte, il reçoit un objet de type « User » contenant son identifiant unique (GUID).
Une fois cet ID obtenu, il l’utilise pour tous les appels à la méthode « GetData »
Implémentation du service
- Nous utilisons et configurons log4net pour afficher les logs à l’aide du ConsoleColoredAppender.
- Nous définissons un business service « UserService » qui contient l’accès aux données et la logique.
- Bien que non indispensable, le framework Unity est utilisé pour l’injection de dépendance entre l’interface du business service et son implémentation
- L’implémentation de la méthode « GetData » proposée par le service WCF contient l’instruction
« Thread.Sleep(waitingTimeInMs); » pour simuler des temps d’exécution aléatoires.
Hébergement du service
Le projet Console « MyCompany.ServiceLauncher » héberge notre service WCF, cela permet notamment de bénéficier de la console et de voir ainsi l’output de log4net directement.
Création du client
Nous créons une application console cliente (MyCompany.Client) qui va appeler le webservice. Afin de simuler des appels autorisés et non-autorisés. Le client se termine quand l’utilisateur appuie sur « ESPACE » ou « ENTER »
class Program { static void Main(string[] args) { Console.WriteLine("Type {ENTER} or {ESCAPE} to exit."); try { using (MyCompanyServiceReference.MyCompanyServiceClient client = new MyCompanyServiceReference.MyCompanyServiceClient()) { var userId = client.Login("ohelin", "test").Id; var wrongUserId = Guid.Empty; //Performs requests do { while (!Console.KeyAvailable) { //Call with valid user try { Random rnd1 = new Random(); var result = client.GetData(rnd1.Next(100), userId); Console.WriteLine(result); } catch (Exception ex) { Console.WriteLine(ex.Message); } //Call with invalid user try { Random rnd1 = new Random(); var result = client.GetData(rnd1.Next(100), wrongUserId); Console.WriteLine(result); } catch(Exception ex) { Console.WriteLine(ex.Message); } Thread.Sleep(100); } } while (Console.ReadKey(true).Key != ConsoleKey.Escape && Console.ReadKey(true).Key != ConsoleKey.Enter); } } catch(Exception ex) { Console.WriteLine(ex.Message); } } } |
Définition et utilisation de l’attribut « LoggerBehavior »
Jusqu’à présent nous avons notre service WCF et son client. Il reste à gérer l’enregistrement des appels à travers la création d’un attribut personnalisé que nous nommerons «LoggerBehavior ».
Il existe 4 types de « behavior » qu’il est possible d’implémenter à travers les interfaces suivantes : IServiceBehavior, IEndpointBehavior, IContractBehavior, IOperationBehavior.
Nous pouvons représenter leur champ d’application de la manière suivante :
A titre d’exemple, il est donc possible de créer un service behavior qui va automatiquement ajouter le « LoggerBehavior » sur toutes les méthodes d’un contrat, d’un endpoint ou même du service entier.
1. Définition du LoggerBehavior :
public class LoggerBehavior : Attribute, IOperationBehavior { public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) { } public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) { //Get all methods parameters names string[] operationParameters = operationDescription.Messages[0].Body.Parts.OrderBy(m => m.Index).Select(m => m.Name).ToArray(); dispatchOperation.ParameterInspectors.Add(new LoggerOperationInspector(operationParameters)); } public void Validate(OperationDescription operationDescription) { } } |
Nous utilisons la méthode ApplyDispatchBehavior pour enregistrer notre OperationInspector qui va traiter la requête en fonction des paramètres passés pendant l’appel.
2. Création du « ParameterInspector » (qui contient la logique du logging) :
public class LoggerOperationInspector : IParameterInspector { /// <summary> /// Business logger /// </summary> private static readonly ILog BusinessLogger = LogManager.GetLogger(typeof(MyCompanyService)); /// <summary> /// Parameter's names /// </summary> public string[] _operationParameters { get; set; } /// <summary> /// Prameter's values /// </summary> public object[] _inputParameters { get; set; } /// <summary> /// Business service to access data /// </summary> public IUserService _userService { get; set; } /// <summary> /// Stopwatch to get execution times /// </summary> public Stopwatch _stopwatch { get; set; } /// <summary> /// Initialization of the Inspector /// </summary> /// <param name="operationParameters"></param> public LoggerOperationInspector(string[] operationParameters) { _stopwatch = new Stopwatch(); //Service to get all users _userService = MyCompanyService.Container.Resolve<IUserService>(); //Keep parameters names _operationParameters = operationParameters; } /// <summary> /// Keep input values then start the stopwatch /// </summary> /// <param name="operationName"></param> /// <param name="inputs"></param> /// <returns></returns> public object BeforeCall(string operationName, object[] inputs) { _inputParameters = inputs; _stopwatch.Restart(); return null; } /// <summary> /// Log methodname, user, execution time /// </summary> /// <param name="operationName"></param> /// <param name="outputs"></param> /// <param name="returnValue"></param> /// <param name="correlationState"></param> public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState) { _stopwatch.Stop(); try { //Get currentuser int position = Array.IndexOf(_operationParameters, "currentUserId"); if (position == -1 || !(_inputParameters[position] is Guid)) { log4net.ThreadContext.Stacks["Method"].Push(operationName); log4net.ThreadContext.Stacks["ExecutionTime"].Push(_stopwatch.ElapsedMilliseconds.ToString()); BusinessLogger.DebugFormat("Operation \"{0}\" was executed in {1} ms", operationName, _stopwatch.ElapsedMilliseconds); log4net.ThreadContext.Stacks["Method"].Pop(); log4net.ThreadContext.Stacks["ExecutionTime"].Pop(); } else { Guid userId = (Guid)_inputParameters[position]; var CurrentUser = _userService.GetUserNameById(userId); log4net.ThreadContext.Stacks["Method"].Push(operationName); log4net.ThreadContext.Stacks["ExecutionTime"].Push(_stopwatch.ElapsedMilliseconds.ToString()); log4net.ThreadContext.Stacks["User"].Push(userId.ToString()); BusinessLogger.DebugFormat("Operation \"{0}\" was executed by the user {1} in {2} ms", operationName, CurrentUser, _stopwatch.ElapsedMilliseconds); log4net.ThreadContext.Stacks["Method"].Pop(); log4net.ThreadContext.Stacks["ExecutionTime"].Pop(); log4net.ThreadContext.Stacks["User"].Pop(); } } catch (Exception ex) { BusinessLogger.Error("Unable to log the time spent in the execution of the method \"" + operationName + "\"", ex); } } } |
La méthode BeforeCall est appelée avant l’exécution de la méthode et nous permet de démarrer notre chronomètre et de garder les paramètres d’entrée en mémoire.
De la même manière, la méthode AfterCall est appelée une fois la méthode exécutée, on peut donc récupérer son temps d’exécution, et en déduire l’utilisateur qui a fait l’appel à travers le paramètre « currentUserId » s’il existe dans la signature.
L’utilisation des ThreadContext de log4net nous permet simplement de déclarer des champs personnalisés supplémentaires pour ne pas à avoir à parser les logs. Cela est particulièrement utile si l’on souhaite créer des statistiques, ou des représentations graphiques pour suivre l’évolution de l’état de santé du service dans le temps.
3. Utilisation de notre attribut
Dans l’interface du service WCF « IMyCompanyService » nous ajoutons simplement l’attribut LoggerBehavior.
/// <summary> /// Returns a simple string /// </summary> /// <param name="value"></param> /// <param name="currentUserId">Logged user</param> /// <returns>String which contains the input parameter named value</returns> [OperationContract] [LoggerBehavior] [FaultContract(typeof(string))] string GetData(int value, Guid currentUserId); |
4. Test du service avec le client
On démarre le service puis on lance le client de test pour qu’il exécute un appel sur la méthode « Login » puis la méthode «GetData ».
Côté service, on peut voir le résultat dans la console d’exécution :
Définition et utilisation de l’attribut « SecurityBehavior »
A l’instar du LoggerBehavior , il est possible de créer un attribut qui se chargera de valider ou non l’accès à une méthode.
1. SecurityBehavior
public class SecurityBehavior : Attribute, IOperationBehavior { public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) { } public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) { //Get all methods parameters names string[] operationParameters = operationDescription.Messages[0].Body.Parts.OrderBy(m => m.Index).Select(m => m.Name).ToArray(); dispatchOperation.ParameterInspectors.Add(new SecurityOperationInspector(operationParameters)); } public void Validate(OperationDescription operationDescription) { } } |
2. SecurityOperationInspector
public class SecurityOperationInspector : IParameterInspector { /// <summary> /// Business logger /// </summary> private static readonly ILog BusinessLogger = LogManager.GetLogger(typeof(MyCompanyService)); /// <summary> /// Parameter's names /// </summary> public string[] _operationParameters { get; set; } /// <summary> /// Prameter's values /// </summary> public object[] _inputParameters { get; set; } /// <summary> /// Business service to access data /// </summary> public IUserService _userService { get; set; } /// <summary> /// Initialization of the Inspector /// </summary> /// <param name="operationParameters"></param> public SecurityOperationInspector(string[] operationParameters) { IUnityContainer container = MyCompanyService.Container; _userService = container.Resolve<IUserService>(); //Keep parameters names _operationParameters = operationParameters; } /// <summary> /// Retrieve the current user and check rights. /// Throw a FaultException if access denied /// </summary> /// <param name="operationName"></param> /// <param name="inputs"></param> /// <returns></returns> public object BeforeCall(string operationName, object[] inputs) { _inputParameters = inputs; int position = Array.IndexOf(_operationParameters, "currentUserId"); if (position > -1 && _inputParameters[position] is Guid) { Guid userId = (Guid)_inputParameters[position]; var CurrentUser = _userService.GetUserNameById(userId); bool isValid = _userService.IsValidUser(userId); if (isValid == false) { log4net.ThreadContext.Stacks["Method"].Push(operationName); log4net.ThreadContext.Stacks["User"].Push(userId.ToString()); BusinessLogger.ErrorFormat("Access Denied for the operation \"{0}\" requested by the user {1} - {2}", operationName, CurrentUser, userId); log4net.ThreadContext.Stacks["Method"].Pop(); log4net.ThreadContext.Stacks["User"].Pop(); //Throw accessdenied exception throw new FaultException(string.Format("Access Denied for the operation \"{0}\" requested by the user {1}", operationName, CurrentUser)); } } return null; } /// <summary> /// No nothing, security check was already processed /// </summary> /// <param name="operationName"></param> /// <param name="outputs"></param> /// <param name="returnValue"></param> /// <param name="correlationState"></param> public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState) { } } |
Nous déclenchons une FaultException si l’accès est refuse et le corps de la méthode ne sera pas exécutée.
4. Test du service avec le client
On démarre le service puis on lance le client de test pour qu’il exécute un appel sur la méthode « Login » puis la méthode «GetData » une fois avec un identifiant correct l’autre avec un identifiant invalide.
Côté service, on peut voir le résultat dans la console d’exécution :
Conclusion
Les applications des attributs dans WCF sont nombreuses et permettent d’éviter la duplication de code et d’ajouter des comportements de manière élégante dans le code.
Comments