C# : application SQL Server XML et ASP.NET Runtime

 

Carl Nolan
Microsoft Corporation

Mars 2002

Résumé: Détails de la construction d’une solution de service client basée sur le web à l’aide de Microsoft .NET Framework, de la fonctionnalité Microsoft SQL Server XML 2000 et des documents XSLT. Avec la prise en charge du runtime Microsoft ASP.NET, le mécanisme d’authentification par formulaire et les classes XML système, la création de systèmes de demande de données XML hautement performants et évolutifs est considérablement simplifiée. (34 pages imprimées)

Téléchargez Csharpsqlxmlhttpapplication.exe.

Contenu

Introduction
The .NET Framework Application
Couche de présentation
Couche de données XML
Couche d'accès aux données
Implémentation de la sécurité
Couche d’application Http
Web Service Extensions
Conclusion

Introduction

Il y a de nombreuses années, j’ai eu la chance de travailler dans la conception d’une solution de service à la clientèle basée sur le web. Avec l’avènement de Microsoft® SQL Server fonctionnalités XML et de Microsoft .NET Framework, j’ai décidé de revenir sur cette solution pour développer une application similaire utilisant microsoft .NET Framework, SQL Server fonctionnalité XML 2000 et des documents XSLT.

La fonctionnalité de l’application était simple : pour permettre aux clients d’afficher les informations sur les clients, les commandes et les détails des commandes dans un environnement sécurisé. L’environnement sécurisé est tel que les utilisateurs authentifiés sont limités à un ensemble de clients défini. Enfin, un ensemble d’utilisateurs administratifs, dont l’objectif est d’aider les utilisateurs génériques via la même interface utilisateur, exigeait un accès aux données sans restriction.

Si vous connaissez SQL Server’accès aux requêtes d’URL, une solution utilisant des requêtes de modèle et des feuilles de style de rendu HTML doit se présenter. Le facteur limitant la solution SQL Server pure serait l’incapacité à fournir un système d’authentification flexible dont dépend le système d’autorisation ; d’où la raison de cette application.

The .NET Framework Application

L’application à présenter se compose des éléments suivants: une architecture de gestionnaire Microsoft ASP.NET Http pour le traitement des demandes, une classe de demande client pour le contrôle des vérifications d’autorisation et du rendu HTML, des documents XSLT pour les transformations XML en HTML, des classes de commande client et de sécurité client, des procédures stockées de retour XML et la couche de base de données physique, en plus d’un mécanisme d’authentification (formulaires).

Figure 1. Schéma Northwind

Le magasin de données utilisé pour l’application était l’exemple de base de données Northwind ; les tableaux pertinents sont présentés dans la figure 1.

Couche de présentation

Le moteur de la conception de l’application était la couche de présentation. Le principe de la solution développée était de présenter, à l’aide d’une interface HTML, des informations sur les détails des clients, des commandes et des commandes à partir de la base de données Northwind. Cela prenait en charge deux types d’utilisateurs, l’utilisateur administratif et l’utilisateur générique.

La première étape de l’application est l’authentification de l’utilisateur, à partir de laquelle l’utilisateur est connecté au système. Pour ce faire, utilisez le mécanisme d’authentification par formulaire .NET qui conserve les informations de connexion de l’utilisateur sous la forme d’un cookie Http. D’autres solutions d’authentification qui auraient pu être utilisées incluent Microsoft Windows® et Microsoft .NET Passport.

Après la connexion, le chemin de présentation dépendait du type d’utilisateur. L’utilisateur administratif est défini comme ayant la capacité d’afficher toutes les informations client. Pour cette raison, la page d’accueil de l’utilisateur administratif est une liste des pays pour lesquels il existe des clients ; La sélection d’un pays dans la liste affiche la liste appropriée des clients. L’utilisateur générique est défini comme ayant accès à une liste prédéfinie de clients. Pour cette raison, la page d’accueil de l’utilisateur générique est la liste des clients pour lesquels l’utilisateur générique a l’autorisation. Une fois qu’une liste de clients a été présentée, la navigation HTML présente une page de détails du client, une liste de commandes client, une page de résumé des détails des commandes et une page complète de résumé des clients et des commandes.

Tableau 1. Flux de présentation

Page Description Opération Procédure stockée XML
Feuille de style
Liste des pays GetCustomersCountries xml_customer_cty_list
customercountry.xslt
Clients pour pays GetCustomersByCountry xml_customer_cty
customerlistcty.xslt
Clients pour l’utilisateur GetCustomersByUser xml_customer_user
customerlistuser.xslt
Détails du client GetCustomerById xml_customer_id
customerheader.xslt
Liste des commandes GetCustomerOrders xml_customer_orders
customerorders.xslt
Détails de la commande GetCustomerOrderDetails xml_order_details
customerorderdetails.xslt
Récapitulatif du client et de la commande GetCustomerSummById xml_customer_summary
customersummary.xslt

L’application crée chaque page HTML à l’aide d’une procédure stockée de retour XML, qui est ensuite transformée en HTML à l’aide d’un document XSLT. Le tableau 1 présente chaque page, le nom de l’opération pour prendre en charge l’extraction XML, le nom des procédures stockées de retour XML associées et enfin le document XSLT correspondant à partir duquel le code HTML est généré.

Couche de données XML

Comme indiqué précédemment, l’application utilise SQL Server fonctionnalité XML 2000, chaque opération du tableau 1 a une procédure stockée de retour XML correspondante. Lors de la conception de ces procédures stockées, la structure du xml résultant devait être prise en compte. Plusieurs techniques peuvent être utilisées, comme le montre cette application.

Pour récupérer des résultats XML directement à partir de la base de données, la clause FOR XML de l’instruction SELECT est utilisée ; avec l’un des trois modes RAW, AUTO et EXPLICIT.

Le mode RAW signifie que chaque ligne du code XML résultant a l’identificateur de ligne générique. Le mode AUTO retourne les résultats de la requête dans une arborescence XML imbriquée simple. Chaque table de la clause FROM, répertoriée dans l’instruction SELECT, est représentée en tant qu’élément XML du même nom. Les colonnes SELECT sont ensuite mappées en tant qu’attributs des éléments. La hiérarchie est déterminée en fonction de l’ordre des tables identifiées par les colonnes de l’instruction SELECT. Pour structurer la colonne identificateurs XML, des noms d’alias doivent être utilisés.

Le mode EXPLICIT spécifie la forme de l’arborescence XML, la requête spécifiant toutes les informations permettant de produire ce que l’on appelle une arborescence universelle. Cela signifie que la requête doit, en plus de spécifier les données requises, toutes les métadonnées. D’autres avantages importants du mode EXPLICIT sont que les colonnes peuvent être mappées individuellement à des attributs ou des sous-éléments et qu’on peut générer des hiérarchies frères.

Lors de l’écriture de requêtes XML, il faut se rappeler que les noms d’identificateur XML respectent la casse, ce qui est important lors de l’exécution des transformations XSLT. En outre, dans les documents XSLT, il faut également se rappeler de préfixer les identificateurs XML avec les symboles @ pour les attributs.

Modification de la hiérarchie XML avec des vues

Le premier défi dans la formulation du code XML est venu lorsque la hiérarchie XML a nécessité un aplatissement ; les informations d’une couche hiérarchique unique sont dérivées de plusieurs tables. Pour ce faire, la requête XML peut utiliser des vues plutôt que des tables sous-jacentes. La vue, étant considérée comme une table unique, aplatit la hiérarchie XML résultante.

Il s’agissait d’une exigence pour la présentation des informations de commande des clients. Dans ce cas, il fallait présenter les informations de commande du client, chaque élément de commande contenant les informations de l’expéditeur :

SELECT Customer.CustomerID CustomerId,
   Customer.CompanyName CompanyName, ContactName, OrderID OrderId,
   CONVERT(CHAR(12),OrderDate, 107) OrderDate,
   CONVERT(CHAR(12),ShippedDate, 107) ShipDate,
   ShipName, Freight,
   (SELECT COUNT(*) FROM [Order Details] OrderDets
      WHERE OrderDets.OrderID = Orders.OrderID) ProductCount,
   Orders.CompanyName ShipCompany, Orders.Phone ShipPhone
FROM Customers Customer
INNER JOIN dbo.view_orders Orders
   ON Customer.CustomerID = Orders.CustomerID
WHERE Customer.CustomerID = @custid
FOR XML AUTO, ELEMENTS

View dans la requête est une requête INNER JOIN simple :

SELECT Orders.*, Shippers.*
FROM Orders
INNER JOIN Shippers ON Orders.ShipVia = Shippers.ShipperID

Dans le code XML généré, la vue est traitée comme une hiérarchie unique ; c’est-à-dire les commandes.

Requêtes FOR XML EXPLICIT

Pour les requêtes nécessitant une structure XML plus complexe, la clause FOR XML EXPLICIT peut être utilisée. Le mode EXPLICIT est utilisé pour les pages de détails de la commande et de résumé des commandes client. Comme indiqué, la structure de la hiérarchie XML est déterminée par une table universelle générée. Par conséquent, l’utilisation des vues n’est pertinente que pour la simplification des requêtes. Prenons l’exemple de la requête pour les informations détaillées de la commande :

SELECT 1 Tag, NULL Parent,
   Orders.CustomerID [CustomerOrder!1!CustomerId],
   Customers.CompanyName [CustomerOrder!1!CompanyName!element],
   Customers.ContactName [CustomerOrder!1!ContactName!element],
   Orders.OrderID [CustomerOrder!1!OrderId],
   CONVERT(CHAR(12),OrderDate, 107) [CustomerOrder!1!OrderDate!element],
   CONVERT(CHAR(12),ShippedDate, 107) [CustomerOrder!1!ShipDate!element],
   Orders.ShipName [CustomerOrder!1!ShipName!element],
   Orders.Freight [CustomerOrder!1!Freight!element],
   Orders.CompanyName [CustomerOrder!1!ShipCompany!element],
   Orders.Phone [CustomerOrder!1!ShipPhone!element],
   Orders.ShipAddress [CustomerOrder!1!ShipAddress!element],
   Orders.ShipCity [CustomerOrder!1!ShipCity!element],
   Orders.ShipPostalCode [CustomerOrder!1!ShipPostalCode!element],
   Orders.ShipCountry [CustomerOrder!1!ShipCountry!element],
   NULL [OrderDetails!2!ProductId],
   NULL [OrderDetails!2!ProductName!element],
   NULL [OrderDetails!2!UnitPrice!element],
   NULL [OrderDetails!2!Quantity!element],
   NULL [OrderDetails!2!DiscountPercent!element],
   NULL [OrderDetails!2!ExtendedPrice!element]
FROM Customers
INNER JOIN dbo.view_orders Orders
   ON Customers.CustomerID = Orders.CustomerID
WHERE Customers.CustomerID = @custid
AND Orders.OrderID = @order

UNION ALL

SELECT 2, 1,
   Orders.CustomerID [CustomerOrder!1!CustomerId],
   NULL [CustomerOrder!1!CompanyName!element],
   NULL [CustomerOrder!1!ContactName!element],
   Orders.OrderID [CustomerOrder!1!OrderId],
   NULL [CustomerOrder!1!OrderDate!element],
   NULL [CustomerOrder!1!ShipDate!element],
   NULL [CustomerOrder!1!ShipName!element],
   NULL [CustomerOrder!1!Freight!element],
   NULL [CustomerOrder!1!ShipCompany!element],
   NULL [CustomerOrder!1!ShipPhone!element],
   NULL [CustomerOrder!1!ShipAddress!element],
   NULL [CustomerOrder!1!ShipCity!element],
   NULL [CustomerOrder!1!ShipPostalCode!element],
   NULL [CustomerOrder!1!ShipCountry!element],
   OrderDetails.ProductID [OrderDetails!2!ProductId],
   OrderDetails.ProductName [OrderDetails!2!ProductName!element],
   UnitPrice [OrderDetails!2!UnitPrice!element],
   Quantity [OrderDetails!2!Quantity!element],
   CAST((Discount*100) AS NUMERIC(7,2))
      [OrderDetails!2!DiscountPercent!element],
   (UnitPrice * Quantity * (1-Discount))
      [OrderDetails!2!ExtendedPrice!element]
FROM dbo.view_orders Orders
INNER JOIN dbo.view_orderdetails OrderDetails
   ON Orders.OrderID = OrderDetails.OrderID
WHERE Orders.CustomerID = @custid
AND Orders.OrderID = @order

ORDER BY [CustomerOrder!1!CustomerId],
   [CustomerOrder!1!OrderId], [OrderDetails!2!ProductId]
FOR XML EXPLICIT

L’une des tâches main dans l’utilisation du mode EXPLICT consiste à s’assurer que les propriétés Tag et Parent sont correctement définies. Par conséquent, dans ce cas, une UNION ALL est utilisée pour retourner les informations requises du client, puis de la commande. Le ORDER BY final est important pour lier correctement les commandes au client approprié. La fonction CAST permet de s’assurer que le type de données correct est retourné à partir de la requête : numeric plutôt que float.

Comme on peut le voir, le mode EXPLICIT, bien que détaillé, offre une plus grande flexibilité dans la génération de l’arborescence universelle résultante. Pour mieux comprendre ce concept de table universelle, exécutez la requête précédente sans la clause FOR XML EXPLICIT. La sortie sera la table universelle.

Couche d'accès aux données

Comme dans la plupart des applications, l’accès à la base de données est contrôlé par le biais d’une couche d’accès aux données ; classe CustomerOrders. L’objectif de cette classe est d’extraire les appels de procédure stockée et de présenter le code XML à l’application Http. Le type de données de retour choisi est un XPathDocument, plutôt qu’un XmlDocument. Les raisons sont double : le code XML obtenu à partir de SQL Server est un fragment de document (peut ne pas avoir de nœud racine) et ne peut donc pas être chargé directement dans un XmlDocument. Ensuite, un XPathDocument offre les meilleures performances pour effectuer des transformations.

Obtention et retour du code XML

La structure de chaque appel de méthode avec la classe est identique : mettez en forme une commande SQLCommand et ses paramètres, définissez sa connexion active, exécutez le lecteur XML et retournez le document XPath construit. Comme seuls les paramètres SQLCommand différencient les appels de méthode d’accès aux données, le traitement XML est géré par une méthode privée :

private XPathDocument CommandToXPath(SqlCommand northwindCom)
{
   // setup the local objects
   SqlConnection northwindCon = null;
   XmlReader xmlReader = null;
   XPathDocument xpathDoc = null;
   // set base command options
   northwindCom.CommandType = CommandType.StoredProcedure;
   northwindCom.CommandTimeout = 15;
   // now execute the command
   try
   {
      // setup the database connection
      northwindCon = new SqlConnection(dbConnectionString);
      northwindCon.Open();
      northwindCom.Connection = northwindCon;
      // execute the command and place into an Xpath document
      xmlReader = northwindCom.ExecuteXmlReader();
      xpathDoc = new XPathDocument(xmlReader, XmlSpace.Preserve);
   }
   catch (Exception ex)
   {
      throw new ApplicationException
         ("Cannot Execute SQL Command: " + ex.Message, ex);
   }
   finally
   {
      // dispose of open objects
      if (xmlReader != null) xmlReader.Close();
      if (northwindCon != null) northwindCon.Close();
   }
   return xpathDoc;
}

La méthode ExecuteXmlReader de la classe SqlCommand retourne un XmlReader représentant le code XML retourné. Ce lecteur est utilisé pour construire le document XPath. Avant d’effectuer cet appel de méthode, la méthode appelante crée une nouvelle commande SqlCommand, spécifie la procédure stockée à exécuter et définit la collection de paramètres appropriée.

Méthodes de classe

Comme vous pouvez le voir à nouveau dans le tableau 1, les méthodes de classe correspondent de 1 à 1 avec une procédure stockée de retour XML et une fonction de présentation d’application web. La figure 2 présente les méthodes publiques et privées pour la classe CustomerOrders. Les méthodes de classe sont toutes des méthodes get simples et retournent une représentation de document XPath du code XML requis.

Figure 2 : Diagramme de classes CustomerOrders

Le seul travail des méthodes de classe consiste à définir la procédure stockée et les paramètres associés. Aucune vérification d’autorisation n’est effectuée, ce qui a été effectué à une étape antérieure. Considérez la méthode pour retourner les détails d’une commande client :

public XPathDocument GetCustomerOrderDetails
   (string customerId, int orderId)
{
   // setup the command object to return the XML Document Fragment
   SqlCommand northwindCom = new SqlCommand("xml_order_details");
   // set up the stored procedure parameters
   SqlParameter customerParam = new SqlParameter
      ("@custid", SqlDbType.NChar, 5);
   customerParam.Direction = ParameterDirection.Input;
   customerParam.Value = customerId;
   northwindCom.Parameters.Add(customerParam);
   SqlParameter orderParam = new SqlParameter
      ("@order", SqlDbType.Int);
   orderParam.Direction = ParameterDirection.Input;
   orderParam.Value = orderId;
   northwindCom.Parameters.Add(orderParam);
   // return the XPath document
   return CommandToXPath(northwindCom);
}

Comme on peut le voir, tout le travail XML est accompli dans la méthode CommandToXPath.

Implémentation de la sécurité

À ce stade, un mot est justifié sur l’implémentation de la sécurité. Comme mentionné précédemment, la classe CustomerOrders ne fait aucune hypothèse concernant la sécurité ; qui est laissé à l’application Http. Les mécanismes de sécurité sont pris en charge par le biais d’entités de base de données, de procédures stockées et d’une classe de sécurité, ainsi que d’ASP.NET mécanisme d’authentification par formulaire.

Tableau 2. Opérations de sécurité

Description Opération
Authentifiez l’utilisateur.
Utilisé sur la page Connexion pour valider le nom d’utilisateur et le mot de passe.
ValidateUserLogin
Obtenir des informations utilisateur.
Utilisé sur la page par défaut pour afficher le nom d’utilisateur et d’autres informations.
GetUserInfo
Autorisez l’utilisateur pour le client.
Pour une demande d’utilisateur générique, validez l’accès en lecture au client demandé.
ValidateUserCustomer
Obtenez la liste des utilisateurs administratifs.
Dans l’application Http, permet de déterminer si l’utilisateur authentifié est un administrateur.
GetAdminUsers

Les opérations de base requises pour prendre en charge l’authentification et l’autorisation des données sont répertoriées dans le tableau 2.

Données physiques

Dans cette application, le modèle de données utilisé est celui de la base de données Northwind. Toutefois, pour les exigences de mappage des utilisateurs aux clients et de prise en charge de l’authentification et de l’autorisation, des extensions étaient nécessaires.

Pour isoler l’implémentation de la sécurité, une base de données distincte appelée NWSecurity a été développée, composée uniquement de deux tables : la première est une table utilisateur dans laquelle les utilisateurs administratifs sont marqués avec un indicateur de bits :

CREATE TABLE dbo.NWUser (
   UserName         NVARCHAR(64)   NOT NULL,
   EmailAddress   NVARCHAR(128)   NOT NULL,
   UserPassword   NVARCHAR(64)   NOT NULL,
   FirstName      NVARCHAR(64),
   LastName         NVARCHAR(64),
   LastAuth         DATETIME         DEFAULT NULL,
   AdminUser      BIT             DEFAULT 0,
   CONSTRAINT PK_NWUser PRIMARY KEY (UserName),
   CONSTRAINT UQ_NWUser_Email UNIQUE (EmailAddress)
)

La deuxième table est une table client utilisateur définissant les affectations de clients. Pour un case activée d’autorisation valide, un utilisateur est un administrateur ou est autorisé à accéder à ce client :

CREATE TABLE dbo.NWCustomer (
   UserName         NVARCHAR(64)   NOT NULL,
   CustomerID      NVARCHAR(5)      NOT NULL,
   LastViewed      DATETIME         DEFAULT NULL,
   AllowAccess      BIT             DEFAULT 1,
   CONSTRAINT PK_NWCustomer PRIMARY KEY (UserName, CustomerID),
   CONSTRAINT FK_NWCustomer_NWUser FOREIGN KEY (UserName)
      REFERENCES NWUser (UserName)
      ON UPDATE CASCADE
      ON DELETE CASCADE
)

La base de données NWSecurity contient plusieurs procédures stockées qui prennent en charge les opérations de sécurité : validation d’un utilisateur pour accéder aux informations client, validation d’un nom d’utilisateur et d’un mot de passe de connexion, récupération des informations utilisateur et récupération d’une liste d’utilisateurs administratifs. Le tableau 3 répertorie ces procédures stockées en fonction de la méthode de classe appelante.

Tableau 3. Procédures stockées de sécurité

Class, méthode Procédure stockée
ValidateUserLogin usp_validate_user_login
GetUserInfo usp_get_user_info
ValidateUserCustomer usp_validate_user_customer_read
GetAdminUsers usp_admin_users

Lors de la conception des procédures stockées, on a supposé que la plupart des utilisateurs du système seraient des utilisateurs génériques. Pour cette raison, une lecture de la table NWCustomer est effectuée de préférence à celle de NWUser, utilisée pour un utilisateur administratif case activée. Cela est important lorsque l’on considère que chaque demande d’un utilisateur générique est précédée d’une autorisation case activée.

Classe de sécurité

Une fois de plus, comme avec la couche d’accès aux données d’application, les méthodes de la classe de sécurité correspondent de 1 à 1 avec une procédure stockée. Une classe CustomerSecurity, comme indiqué dans la figure 3, agit comme une couche d’abstraction entre l’application et les données physiques.

Figure 3. Diagramme de classes CustomerSecurity

Dans le cas de ValidateUserLogin, la valeur renvoyée est une énumération :

public enum LoginReturnCode
{
   NoAccess = 0,
   AccessIncorrectPassword = 1,
   AccessAuthenticated = 2
}

Les valeurs de l’énumération correspondent aux valeurs retournées par la procédure stockée correspondante. Ainsi, dans ce scénario, la valeur de retour des procédures stockées non-requête peut être convertie en type enum :

securityCom.ExecuteNonQuery();   
LoginReturnCode lrcValue = (LoginReturnCode)returnParam.Value;

La méthode GetAdminUsers diffère des autres en ce qu’elle retourne un tableau simple de noms d’utilisateurs administratifs :

public Array GetAdminUsers()
{
   // define array list to hold user names
   ArrayList userList = new ArrayList();
   using (SqlConnection securityCon = GetDbConnection())
   {
      using (SqlCommand securityCom =
         new SqlCommand("usp_admin_users", securityCon))
      {
         securityCom.CommandType = CommandType.StoredProcedure;
         // execute the command to obtain the resultant dataset
         SqlDataReader dataNW =
            securityCom.ExecuteReader(CommandBehavior.CloseConnection);
         // with the data reader parse values into a searchable array
         while(dataNW.Read())
         {
            userList.Add((string)dataNW["UserName"]);
         }
         dataNW.Close();
      }
   }
   // convert array list into an Array and return
   Array userArray = userList.ToArray(typeof(String));
   Array.Sort(userArray);
   return userArray;
}

À l’aide de SqlConnection et SqlCommand (objets supprimés lorsqu’ils sortent de l’étendue), un SqlDataReader est construit. Le lecteur de données est ensuite analysé pour obtenir la liste des noms d’utilisateurs, leurs valeurs étant placées dans une liste de tableau. La liste de tableaux est ensuite simplifiée en tant que tableau de variables de chaîne contenant des noms d’utilisateur.

Authentification par formulaire

Le principe de l’authentification est la validation d’un utilisateur pour l’accès au système. Pour tous les utilisateurs, qu’il s’agisse d’un utilisateur administratif ou générique, l’authentification sur la table de base de données utilisateur est effectuée.

Pour forcer l’authentification, une implémentation de formulaires personnalisés a été implémentée. Dans l’application web Web.config fichier, les sections d’authentification et d’autorisation sont modifiées pour forcer l’authentification par formulaire et refuser l’accès à l’utilisateur anonyme ; Les autres mécanismes pris en charge sont l’authentification Windows et Passport :

<authentication mode="Forms">
   <forms name="CustomerServiceApp" loginUrl="login.aspx"
      protection="All" timeout="30" path="/" />
</authentication>
<authorization>
   <deny users="?" />
</authorization>

La tâche suivante consistait à écrire la page login.aspx pour valider les utilisateurs par rapport à la base de données. L’objectif de cette page est de permettre à l’utilisateur d’entrer un nom d’utilisateur et un mot de passe, de les valider par rapport à la base de données, de conserver un jeton d’utilisateur dans un cookie pour un appel ultérieur, puis de rediriger l’utilisateur vers la page demandée à l’origine. Là encore, le .NET Framework rend toutes ces tâches étonnamment simples.

L’interface utilisateur de la page est très simple, deux zones de texte, l’une pour le nom d’utilisateur et l’autre pour le mot de passe, une zone de case activée pour permettre à l’utilisateur de décider si l’authentification sera conservée entre les sessions du navigateur, et enfin un bouton Connexion ; toutes sont des balises HTML standard avec une balise RUNAT du serveur. Des contrôles de formulaire Web côté serveur simples sont utilisés pour garantir que les valeurs sont entrées dans les champs nom d’utilisateur et mot de passe. En outre, une étiquette côté serveur est utilisée pour les commentaires à l’utilisateur.

Tout le travail réel de l’authentification est effectué dans le code côté serveur. Lors de la publication de la page, les informations d’identification entrées sont validées à l’aide de la méthode ValidateUserLogin de la classe CustomerSecurity :

// get required query string parameters
string userName = UserName.Value;
string userPassword = UserPassword.Value;
// create a customer security object and make call to validate the user
CustomerSecurity customerSecurity = new CustomerSecurity();
CustomerSecurity.LoginReturnCode loginReturn =
   customerSecurity.ValidateUserLogin(userName, userPassword);

Ici, UserName et UserPassword sont les contrôles côté serveur initialisés. Si le code de retour de connexion approprié est reçu, l’utilisateur est authentifié et redirigé vers la page demandée :

if (loginReturn == CustomerSecurity.LoginReturnCode.AccessAuthenticated)
{
   FormsAuthentication.RedirectFromLoginPage
      (UserName.Value, PersistForms.Checked);
}

La méthode statique RedirectFromLoginPage redirige l’utilisateur vers la page initialement demandée, après l’émission d’un ticket d’authentification. Le deuxième paramètre accepte une valeur booléenne spécifiant si un cookie durable est émis ou non. Elle est dérivée de la zone de case activée HTML PersistForms.

Le dernier élément important du mécanisme d’authentification consiste à déterminer les rôles dans lesquels placer l’utilisateur authentifié. Le fichier global.asax de l’application est un événement d’authentification qui permet de redéfinir un principal d’utilisateur à partir de l’identité d’utilisateur d’origine. À ce stade, des rôles d’utilisateur peuvent être définis.

protected void Application_AuthenticateRequest
   (Object sender, EventArgs e)
{
   HttpContext context = HttpContext.Current;
   if (!(context.User == null))
   {
      if (context.User.Identity.AuthenticationType == "Forms" )
      {
         string userName = context.User.Identity.Name;
         string[] userRoles = new string[1];
         // define the role based on locating a admin user
         if (Array.BinarySearch(GetAdminUsers(Context), userName) >= 0)
         {
            userRoles[0] = "Admin";
         }
         else
         {
            userRoles[0] = "Generic";
         }
         // create the new generic principal
         GenericPrincipal gp = new GenericPrincipal
            (context.User.Identity, userRoles);
         context.User = gp;
      }
   }
}

Le rôle d’utilisateur est défini comme Administratif ou Générique. La méthode GetAdminUsers met en cache et retourne un tableau d’utilisateurs administratifs ; plus d’informations à ce sujet ci-dessous.

Couche Application Http

L’application Http utilise la prise en charge du runtime http ASP.NET ; un remplacement logique pour les API d’extension et de filtre ISAPI, ce qui permet d’interagir avec les services de requête et de réponse de bas niveau du serveur Web Microsoft IIS. Voici les interfaces clés utilisées dans l’application :

  • IHttpHandler : implémenté pour traiter les requêtes Http synchrones. La méthode ProcessRequest doit être implémentée pour fournir une exécution d’URL personnalisée.
  • IHttpAsyncHandler : implémenté pour traiter les requêtes Http asynchrones. La méthode ProcessRequest est implémentée via les méthodes BeginProcessRequest et EndProcessRequest.
  • IHttpHandlerFactory : implémenté pour créer des objets IHttpHandler. Le seul objectif est de fabriquer dynamiquement de nouveaux objets de gestionnaire qui implémentent l’interface IHttpHandler.

Une implémentation IHttpHandlerFactory traite chaque requête en examinant l’utilisateur authentifié. Pour les utilisateurs administratifs, une implémentation IHttpHandler appelée CustomerAdmin est retournée. Pour les utilisateurs génériques, une implémentation IHttpAsyncHandler appelée CustomerGeneric est retournée.

Pour prendre en charge les implémentations du gestionnaire, une classe CustomerRequest est utilisée comme indiqué dans la figure 4. Cette classe a une méthode exposée publiquement nommée ProcessRequest. À l’aide du httpContext fourni, il passe en revue la requête Http et génère le code HTML requis. Cette classe utilise à son tour les classes CustomerOrders et CustomerSecurity pour l’accès aux données et la vérification de la sécurité.

Figure 4. Diagramme de classe CustomerRequest

Comme une seule application gestionnaire Http traite toutes les requêtes, la fonction appropriée et le code HTML rendu sont déterminés par un paramètre Function passé dans la chaîne de requête d’URL ; la page d’accueil de l’utilisateur approprié s’affiche lorsque la fonction n’est pas présente. Le tableau 4 décrit ces codes de fonction et les méthodes CustomerOrders de prise en charge.

Tableau 4. Codes de fonction d’application

Fonction d’administration Fonction utilisateur générique Méthodes de classe
null   GetCustomersCountries
CC   GetCustomersByCountry
  null GetCustomersByUser
CH CH GetCustomerById
Système d’exploitation Système d’exploitation GetCustomerOrders
OD OD GetCustomerOrderDetails
CS CS GetCustomerSummById

En fonction de ce paramètre de fonction, la méthode de classe appropriée est appelée, qui retourne la représentation XML des données. Celui-ci est ensuite transformé en HTML à l’aide du document XSLT approprié et transmis dans le flux de réponse HTTP.

Implémentations IHttpHandler

Pour un utilisateur administratif, un gestionnaire CustomerAdmin instance est retourné qui implémente IHttpHandler. Pour un utilisateur générique, le gestionnaire CustomerGeneric est retourné qui implémente IHttpHandlerAsync.

Comment déterminer un utilisateur administratif ? C’est là que le concept de programmation basée sur les rôles entre en jeu. Il suffit d’examiner le rôle d’utilisateur précédemment défini :

if (context.User.IsInRole("Admin")) >= 0)
{
   return (new CustomerAdmin());
}
else
{
   return (new CustomerGeneric());
}

Avant de décrire l’implémentation du gestionnaire, examinons l’interface IHttpHandler :

public interface IHttpHandler
{
   void ProcessRequest(HttpContext context);
   bool IsReusable {get;}
}

La méthode unique ProcessRequest est appelée pour chaque requête Http. L’objet HttpContext donné fournit des références aux objets serveur intrinsèques utilisés pour traiter les requêtes Http ; par exemple, Request, Response, Session et Server. La propriété IsReusable indique si le instance IHttpHandler est réutilisable. Dans les deux implémentations fournies, le gestionnaire est toujours considéré comme réutilisable, sauf si une exception d’application est levée.

Pour la gestion des exceptions, une classe dérivée d’ApplicationException est définie. L’objectif de la nouvelle classe Exception est de fournir une propriété, Terminated, que l’implémentation du gestionnaire utilise pour déterminer si le gestionnaire doit rester dans le pool. Une valeur true indique une exception de traitement telle que l’objet gestionnaire doit être supprimé du pool de traitement.

L’implémentation IHttpHandler pour un utilisateur administratif, CustomerAdmin, est répertoriée ci-dessous :

public class CustomerAdmin: IHttpHandler 
{
   private bool reuseHandler;
   private CustomerRequest customerRequest;
      
   public CustomerAdmin() 
   {
      // ensure object is to be pooled
      reuseHandler = true;
      // cache the user customer request object
      customerRequest = new CustomerRequest();
   }

   // property to indicate class reuse state
   public bool IsReusable
   {
      get 
      {
         return reuseHandler;
      }
   }

   // process the HTTP request called by the application process
   public void ProcessRequest(HttpContext context) 
   {
      try 
      {
         customerRequest.ProcessRequest(context);
      }
      catch (CustomerRequestException ex) 
      {
         // take handler out of the pool if the application error
         reuseHandler = !ex.Terminated;
         CustomerRequestUtilities.ProcessException(context, ex);
      }
      catch (Exception ex) 
      {
         // take handler out of the pool and display an error page
         reuseHandler = false;
         CustomerRequestUtilities.WriteTraceOutput
            (context, "Process", ex.Message);
         CustomerRequestUtilities.ProcessException(context, ex);
      }
   }
}

La méthode ProcessRequest crée un instance de la classe CustomerRequest, en appelant la méthode ProcessRequest correspondante pour traiter la requête Http. Si l’objet CustomerRequest lève une exception CustomerRequestException, le gestionnaire a la possibilité de décider si le gestionnaire doit rester dans le pool d’exécution.

En revanche, l’implémentation du gestionnaire pour les utilisateurs génériques implémente l’interface IHttpAsyncHandler. Cette interface a une méthode BeginProcessRequest qui doit lancer un appel asynchrone au gestionnaire Http. Pour les demandes d’utilisateur génériques, un processus distinct effectue des vérifications d’autorisation. Étant donné que cette case activée est effectuée de manière asynchrone, il est judicieux de présenter le gestionnaire asynchrone à partir de l’implémentation utilisateur générique.

Pour effectuer l’appel asynchrone, un délégué est défini pour la méthode ProcessRequest. Les méthodes BeginInvoke et EndInvoke générées par le compilateur sont ensuite utilisées pour appeler la méthode ProcessRequest d’un instance CustomerRequest, de façon asynchrone :

internal delegate void ProcessRequestDelegate(HttpContext context);

// start the processing of the async HTTP request
public IAsyncResult BeginProcessRequest
   (HttpContext hc, AsyncCallback cb, Object extraData) 
{
   // save the callback reference
   callback = cb;
   context = hc;

   // start the async operation to handle the customer request
   try 
   {
      // create the delegate and reference the callback method
      ProcessRequestDelegate processDelegate = new ProcessRequestDelegate
         (customerRequest.ProcessRequest);
      AsyncCallback processCallback = new AsyncCallback
         (this.ProcessRequestResult);
      // call the compiler created begin invoke method
      IAsyncResult result = processDelegate.BeginInvoke
         (context, processCallback, this);
   }
   catch (Exception ex) 
   {
      // take handler out of the pool and display an error page
      // cannot start the async process - infrastructure error
      reuseHandler = false;
      CustomerRequestUtilities.WriteTraceOutput
      (context, "Process", ex.Message);
      throw ex;
   }

   // return my async result indicating the calling status
   processAsyncResult = new ProcessAsyncResult();
   processAsyncResult.AsyncState = extraData;
   return processAsyncResult;
}

// function to be called upon completion
internal void ProcessRequestResult(IAsyncResult result) 
{
   try 
   {
      // obtain a reference to the original calling class
      ProcessRequestDelegate processCallback = (ProcessRequestDelegate)
         ((AsyncResult)result).AsyncDelegate;
      // call the end invoke capturing any runtime errors
      processCallback.EndInvoke(result);
   }
   catch (CustomerRequestException ex) 
   {
      // take handler out of the pool if the application error
      reuseHandler = !ex.Terminated;
      CustomerRequestUtilities.ProcessException(context, ex);
   }
   catch (Exception ex) 
   {
      // take handler out of the pool and display an error page
      reuseHandler = false;
      CustomerRequestUtilities.WriteTraceOutput
         (context, "Process", ex.Message);
      CustomerRequestUtilities.ProcessException(context, ex);
   }
   finally 
   {
      processAsyncResult.IsCompleted = true;
      callback(processAsyncResult);
   }
}

Comme on peut le voir, la requête Http est en cours de traitement en deux parties. La demande BeginProcessRequest lance l’appel asynchrone par le biais de la méthode BeginInvoke des délégués ; À ce stade, les erreurs sont du type d’infrastructure. Par le biais de l’objet délégué et de rappel, endInvoke est appelé à l’achèvement du processus asynchrone pour obtenir des données de retour ; y compris les exceptions.

La status du gestionnaire Http est connue de la classe appelante via une implémentation de l’interface IAsyncResult, appelée ProcessAsyncResult.

CustomerRequest, classe et rendu HTML

L’objectif des classes de gestionnaire Http est de gérer un objet CustomerRequest qui effectue le rendu HTML et les vérifications d’autorisation. Il utilise la classe CustomerOrders pour obtenir les informations XML et les transforme en HTML à l’aide du document XSLT approprié. Une seule méthode exposée, ProcessRequest, est utilisée :

public void ProcessRequest(HttpContext context) 
{
   // define initial state of the object
   validProcess = 0;
   this.context = context;
   userType = context.User.IsInRole("Admin")?
      UserType.AdminUser : UserType.GenericUser;

   // obtain function code as no security check is required for null
   string functionCode = context.Request.QueryString["Function"];

   // look to see if authorization is required and start the process
   bool performAuthorization;
   if (userType == UserType.GenericUser && functionCode != null) 
   {
      performAuthorization = true;
      // start the thread that performs the security validation
      validSecurity = 1;
      threadSecurity.Start();
   }
   else 
   {
      performAuthorization = false;
      // admin user or null funciton code so security always true
      validSecurity = 0;
   }

   // get the customer XML data and associated stylesheet name
   XPathDocument docCust;
   XslTransform docStyle;
   ReturnCustomerXml(out docCust, out docStyle);

   // if performed a security check join with processing thread
   if (performAuthorization) 
   {
      // join with the security thread with a timeout of 5 seconds
      if (!threadSecurity.Join(2500)) 
      {
         validProcess = 2;
         CustomerRequestUtilities.WriteTraceOutput
            (context, "Security", "Unable to Complete Security Check");
         try 
         {
            threadSecurity.Abort();
         }
         catch (Exception) {}
      }
   }

   // if all process and security valid output the required HTML
   if (validSecurity == 0 && validProcess == 0) 
   {
      // output the required XML and performing the transformation
      docStyle.Transform(docCust, null, context.Response.Output);
   }
   else 
   {
      bool terminated = (validSecurity < 2 && validProcess < 2)?
         false : true;
      // on error throw exception (traces will have been written)
      throw new CustomerRequestException
         ("Process or Security Error encountered.", terminated);
   }
}

La méthode utilise deux valeurs à trois états, validProcess et validSecurity, pour indiquer l’état du traitement : 0 indique que tout est valide, 1 indique qu’aucune erreur n’est levée mais ne peut pas traiter la demande et 2 indique les erreurs de traitement rencontrées.

Il se trouve dans la méthode ReturnCustomerXML privée dans laquelle la méthode CustomerOrders appropriée est appelée : un XPathDocument et XslTransform chargé sont les sorties. La méthode Transform du XslTransform génère ensuite le code HTML dans le flux de réponse :

private void ReturnCustomerXml
   (out XPathDocument docCust, out XslTransform docStyle)
{
   // define the return values
   docCust = null;
   docStyle = null;
   string styleName = "";

   try
   {
      // define variables for function calls
      string functionCode = context.Request.QueryString["Function"];
      string userName, customerId;
      string countryCode;
      int orderNumber;
      // construct the appropriate XPath Document from function
      switch (functionCode)
      {
         case null:
            if (userType == UserType.AdminUser)
            {
               // obtain a XPath Document of customer countries
               docCust = customerOrder.GetCustomersCountries();
               styleName = "customercountry.xslt";
            }
            else
            {
               // obtain a XPath Document of customer user listing
               userName = context.User.Identity.Name;   
               docCust = customerOrder.GetCustomersByUser(userName);
               styleName = "customerlistuser.xslt";
            }
            break;
         case "cc":
            if (userType == UserType.AdminUser)
            {
               // obtain a XPath Document of customer listing
               countryCode = context.Request.QueryString["Country"];      
               docCust = customerOrder.GetCustomersByCountry
                  (countryCode);
               styleName = "customerlistcty.xslt";
            }
            else
            {
               validProcess = 1;
               CustomerRequestUtilities.WriteTraceOutput
                  (context, "Process", "Function cc not available");
            }
            break;
         case "cs":
            // obtain a XPath Document of customer summary
            customerId = context.Request.QueryString["Customer"];      
            docCust = customerOrder.GetCustomerSummById(customerId);
            styleName = "customersummary.xslt";
            break;
         case "ch":
            // obtain a XPath Document of customer header information
            customerId = context.Request.QueryString["Customer"];      
            docCust = customerOrder.GetCustomerById(customerId);
            styleName = "customerheader.xslt";
            break;
         case "os":
            // obtain a XPath Document of order summary information
            customerId = context.Request.QueryString["Customer"];      
            docCust = customerOrder.GetCustomerOrders(customerId);
            styleName = "customerorders.xslt";
            break;
         case "od":
            // obtain a XPath Document of order detail information
            customerId = context.Request.QueryString["Customer"];      
            orderNumber = Int32.Parse
               (context.Request.QueryString["Order"]);      
            docCust = customerOrder.GetCustomerOrderDetails
               (customerId, orderNumber);
            styleName = "customerorderdetails.xslt";
            break;
         default:
            validProcess = 1;
            CustomerRequestUtilities.WriteTraceOutput
               (context, "Process", "Unknown Function Code");
            break;
      }
   }
   catch (Exception ex)
   {
      validProcess = 2;
      CustomerRequestUtilities.WriteTraceOutput
         (context, "Process", "Error: " + ex.Message);
   }
   // load the appropriate stylesheet for the transform
   if (validProcess == 0) docStyle = GetStyleSheet(context, styleName);
   return;
}

Le principe de la méthode ReturnCustomerXml est simple ; à l’aide du code de fonction demandé, appelez la méthode CustomerOrders appropriée pour obtenir le XPathDocument et chargez le XslTransform requis à partir du nom de fichier XSLT dérivé. Pour accélérer le traitement, les documents XSLT sont préchargés et conservés dans le cache de l’application.

Dans cette classe, on remarquera la méthode WriteTraceOutput ; son objectif est d’écrire des informations dans le fichier de trace. Cela fournit un mécanisme permettant d’afficher les messages d’information concernant le traitement des exceptions via la page trace.axd.

Autorisation de l'utilisateur

L’autorisation diffère de l’authentification en ce qu’elle valide toutes les demandes, non pour l’accès à l’application, mais pour garantir que l’utilisateur est limité à un sous-ensemble défini de données. Pour ce faire, un utilisateur est limité à un ensemble de clients affectés, comme défini dans la table NWCustomer.

Pour les utilisateurs génériques, un thread de traitement dédié est utilisé pour effectuer des vérifications d’autorisation via la méthode ValidateCustomerSecurity. La validité du case activée est indiquée par la valeur à trois états appropriée :

private void ValidateCustomerSecurity()
{
   // query string variables
   string customerId = context.Request.QueryString["Customer"];
   string userName = context.User.Identity.Name;
   if (customerId != null && userName != null)
   {
      try 
      {
         // check against the database for the return code
         if (customerSecurity.ValidateUserCustomer(userName, customerId))
         {
            validSecurity = 0;
         }
         else
         {
            validSecurity = 1;
            CustomerRequestUtilities.WriteTraceOutput
               (context, "Security", "Customer/User not Valid");
         }
      }
      catch (Exception ex)
      {
         validSecurity = 2;
         CustomerRequestUtilities.WriteTraceOutput
            (context, "Security", "Error: " + ex.Message);
      }
   }
   else
   {
      validSecurity = 1;
      CustomerRequestUtilities.WriteTraceOutput
         (context, "Security", "Customer/User not Specified");
   }
   return;
}

Lors du traitement de la requête Http, le thread de sécurité est démarré et joint ultérieurement au thread de traitement à l’aide de la méthode Join. À ce stade, le status de sécurité est validé avant le rendu du code HTML.

Regroupement d’objets et mise en cache d’applications

À deux reprises, il a été mentionné que des données statiques ont été mises en cache dans l’application pour un tableau d’utilisateurs administratifs et des documents XSLT chargés. Les performances nettement améliorées sont remarquées lorsque les données sont mises en cache plutôt traitées pour chaque requête.

Le HttpContext actuel gère une référence à un objet Cache d’application. Les données mises en cache dans cet objet peuvent être marquées comme non valides par différents mécanismes, notamment un intervalle de temps défini, une heure absolue et un événement de notification de modification de fichier.

L’événement Authentication utilise le cache de contexte pour le tableau d’utilisateurs administratifs : le tableau est obtenu et chargé par la méthode GetAdminUsers :

private Array GetAdminUsers(HttpContext context)
{
   string adminUsersFile = "adminusers.xml";

   // obtain a reference to the admin users list from the cache
   Array usersArray = (Array)context.Cache[adminUsersFile];
   // the the cached items does not exist the load from the server
   if (usersArray == null)
   {
      try
      {
         // get the current list of the admin users
         CustomerSecurity customerSecurity = new CustomerSecurity();
         usersArray = customerSecurity.GetAdminUsers();
      }
      catch (Exception ex)
      {
         // if an error just create a blank array
         // so the cache will not try and load for each request
         usersArray = new ArrayList().ToArray(typeof(String));
         CustomerRequestUtilities.WriteTraceOutput
            (context, "Security", ex.Message);
      }
      // place the admin users array in cache for 20 minutes
      string adminUsersPath = context.Server.MapPath
         ("//CustService/adminusers.xml");
      context.Cache.Insert(adminUsersFile, usersArray,
         new CacheDependency(adminUsersPath),
         DateTime.Now.AddMinutes(20), TimeSpan.Zero);
   }
   // return the array of admin users
   return usersArray;
}

Notez également que le cache a une dépendance sur un fichier XML en plus d’une expiration de 20 minutes. Le code inclut un script qui génère ce fichier. Vous pouvez utiliser cela comme mécanisme pour actualiser le cache lors de la modification de la liste des utilisateurs administratifs.

De façon très similaire, la classe CustomerOrders a une méthode GetStyleSheet qui retourne un document XSLT chargé à partir du cache. Dans ce cas, le code est très similaire à la fonctionnalité de tableau d’utilisateurs d’administration. Initialement, l’objet cache est obtenu et converti en objet XslTransform :

XslTransform docStyle = (XslTransform)context.Cache[styleName];

Si le cache est vide, n’est jamais chargé ou invalidé, le document XSLT approprié est chargé et placé dans le cache. Dans ce cas, le cache dépend uniquement du fichier à partir duquel le XslTransform a été chargé :

if (docStyle == null)
{
   // from the style sheet name get the required style sheet path
   string stylePath = context.Server.MapPath
      ("//CustService/" + styleName);
   // load the required style sheet
   docStyle = new XslTransform();
   docStyle.Load(stylePath);
   // place the style sheet into the cache
   context.Cache.Insert(styleName, docStyle,
      new CacheDependency(stylePath));
}

Une alternative à l’utilisation du cache de contexte consiste à mettre en cache toutes les données appropriées dans le gestionnaire ou les variables d’étendue d’application mis en pool. Les avantages du cache de contexte sont les mécanismes prédéfinis par lesquels le cache peut être marqué non valide et donc rechargé automatiquement.

En plus de la mise en cache de contexte, l’application utilise le fait que les gestionnaires Http sont mis en pool. Pour prendre en charge cela, chaque constructeur de gestionnaire crée une instance de la classe CustomerRequest, qui à son tour crée et instance de la classe CustomerOrders et CustomerSecurity. En outre, la classe CustomerOrders crée un objet Thread, utilisé pour les vérifications d’autorisation.

Dans l’utilisation de cette méthode de conservation des références d’objet, il est important que chaque requête, gérée par la méthode ProcessRequest, soit sans état ; chaque requête de gestionnaire Http doit initialiser toutes les variables de traitement locales. En outre, si une erreur est rencontrée, l’objet gestionnaire Http est marqué comme non réutilisable, ce qui force la génération d’un nouveau gestionnaire et de tous les objets associés.

Les classes CustomerOrders et CustomerSecurity ne contiennent pas connexions aux bases de données, mais simplement la chaîne de connexion lue à partir du fichier de configuration. Cela permet au regroupement de ressources OLEDB de gérer les connexions aux bases de données.

Web Service Extensions

Jusqu’à présent, l’application a travaillé sur le principe que le consommateur utilisera l’application web pour accéder aux informations sur les clients et les commandes. Avec l’avènement de l’infrastructure des services web .NET Framework, une autre solution est plausible : autoriser le retour du XML à partir d’une méthode de service web, ce qui permet au consommateur d’intégrer l’application dans les propres systèmes du client.

Méthodes et attributs web

Le principe du service web est simple : exposez les méthodes de classe principales qui retournent les informations sur le client et la commande. La figure 5 présente la définition de la classe CustomerWebService pour prendre en charge cette exigence.

Figure 5. Diagramme de classe de service web

Chaque méthode de classe utilise la classe CustomerOrder pour obtenir le code XML résultant (sous la forme d’un XPathDocument) et retourne ce fichier sous la forme d’un xmlElement que l’appelant peut utiliser en fonction des besoins. L’hypothèse est toujours que l’utilisateur est authentifié et connu ; plus d’informations sur ce qui suit. Par exemple, prenez la méthode GetCustomerOrderDetails :

[WebMethod(Description="Obtain the Customer Order Summary Information")]
[SoapHeader("soapCredentials",
   Required=false, Direction=SoapHeaderDirection.In)]
[SoapHeader("soapTicket",
   Required=true, Direction=SoapHeaderDirection.InOut)]
public XmlElement GetCustomerOrders(string CustomerID)
{
   // first validate the user has access to the customer
   ValidateCustomerSecurity(CustomerID);
   // call the method to get the customer information
   CustomerOrders customerOrders = new CustomerOrders();
   XPathDocument xpathCustDoc =
      customerOrders.GetCustomerOrders(CustomerID);
   // return the string representation of the XML
   return GetElement(xpathCustDoc);
}

Les attributs SoapHeader et ValidateCustomerSecurity traitent tous de la sécurité, de l’authentification des utilisateurs et de leur autorisation pour les données client. La méthode GetElement prend le XPathDocument de la classe CustomerOrders et le transforme en xmlElement requis :

private XmlElement GetElement(XPathDocument xpathCustDoc)
{
   // load the required style sheet to transform XPath into XML
   string xsltDocumentPath = Context.Request.PhysicalApplicationPath
+ "customerxmlnode.xslt";
   XslTransform docStyle = new XslTransform();
   docStyle.Load(xsltDocumentPath);
   XmlDocument docXml = new XmlDocument();
   // create a new string writer for the transform
   StringWriter stringWriter = new StringWriter();
   // perform and return the XmlDocument representation of the XML
   // assuming the document has a root node
   docStyle.Transform(xpathCustDoc, null, stringWriter);
   docXml.LoadXml(stringWriter.ToString());
   // return document element
   return docXml.DocumentElement;
}

Cette méthode prend un XPathDocument et effectue une transformation simple à l’aide d’un document XSLT :

<?xml version="1.0" encoding='UTF-8' ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" />
   <xsl:template match="/ | @* | node()">
      <xsl:copy-of select="@* | node()" />
   </xsl:template>
</xsl:stylesheet>

L’objectif de ceci est simplement d’obtenir la représentation sous forme de chaîne du XpathDocument, à partir de laquelle un XmlDocument et son élément racine peuvent être obtenus. Cette transformation suppose que le fragment de document XPath a un nœud racine. Si ce n’était pas le cas, le document XSLT aurait été restructuré pour en inclure un.

Authentification SOAP

Comme vous pouvez le voir, l’implémentation réelle du service web est simple ; mais qu’en est-il de la sécurité ? Il existe de nombreuses options pour gérer l’authentification et l’autorisation dans les services Web. Dans l’implémentation ci-dessus, des en-têtes SOAP personnalisés sont utilisés. Une solution à un en-tête SOAP personnalisé est un HttpModule qui authentifie l’utilisateur via l’en-tête SOAP. La technique que j’ai choisie était un peu différente.

J’ai défini deux en-têtes SOAP dans le cadre du service web. L’en-tête SOAPCredentials facultatif est utilisé pour transmettre les informations d’identification utilisateur validées par rapport à la base de données utilisateur. Une fois l’authentification réussie, un jeton d’autorisation est créé et est une version chiffrée du nom d’utilisateur avec une heure d’expiration et est placé dans l’en-tête SOAPTicket requis. C’est ce ticket qui est déchiffré et validé lors des appels de méthode web suivants, enregistrant un aller-retour dans la base de données pour effectuer l’authentification.

Conclusion

Espérons que la solution présentée ici démontre la puissance de Microsoft .NET Framework pour l’écriture d’applications qui utilisent la fonctionnalité XML de SQL Server 2000. Avec la prise en charge du runtime ASP.NET, le mécanisme d’authentification par formulaire et les classes XML système, la création de systèmes d’interrogation basés sur xml hautes performances et évolutifs a été considérablement simplifiée.

En outre, de nouvelles possibilités existent avec l’inclusion de l’infrastructure des services Web. Les applications web précédemment exposées peuvent désormais être facilement exposées en tant que services web. Cela permet aux consommateurs d’intégrer facilement les fonctionnalités fournies dans leurs propres applications.

Références

SQL Server et SQL Server XML

Page d’accueil XML sur MSDN

Technologies de mappage SQLXML et XML sur MSDN

Enquête sur les fonctionnalités XML de Microsoft SQL Server 2000

SQL Server 2000 : Les nouvelles fonctionnalités offrent une facilité d’utilisation et une scalabilité inégalées aux administrateurs et aux utilisateurs

développement ASP.NET

Page d’accueil Développement .NET sur MSDN

page ASP.NET sur MSDN

ASP.NET Architecture

Authentification et autorisation

Authentification par formulaire simple

Carl Nolan travaille en Californie du Nord au Microsoft Technology Center dans la Silicon Valley. Ce centre se concentre sur le développement de solutions Microsoft .NET à l’aide de la plateforme Microsoft Windows .NET.