作者:Carl Nolan (Microsoft Corporation)
[摘要]详述使用Microsoft .NET Framework、Microsoft SQL Server 2000 XML功能和XSLT文档的基于Web的用户服务解决方案的结构。利用Microsoft ASP.NET运行时支持、窗体身份验证机制和系统XML类,可以极大地简化高性能且可伸缩的基于XML的查询系统的创建。
很多年前,我曾有幸参加了基于Web的客户服务解决方案的设计。由于Microsoft SQL Server XML功能和Microsoft .NET Framework的出现,我决定再次重新审视该解决方案,开发一个利用Microsoft .NET Framework、SQL Server 2000 XML功能和XSLT文档的相似应用程序。这个应用程序的功能很简单:使用户能够在一个安全的环境中查看客户、定单以及定单详细内容信息。安全环境是这样的,已验证身份的用户限于一个已定义的客户集。最后,一组管理员——他们的目的是通过相同的用户界面对一般用户提供协助——需要能够对数据进行无限制的访问。如果您熟悉SQL Server URL查询访问,则可以采用使用模板查询和HTML呈现样式表的解决方案。纯SQL Server解决方案的限制因素是不能够提供授权系统所依赖的一个灵活的身份验证系统,这就是开发该应用程序的原因。
将要介绍的这个应用程序包括下列部分:一个用来处理请求的Microsoft ASP.NET Http处理程序体系结构、一个用来控制授权检查和HTML呈现的客户请求类、用于XML到HTML转换的XSLT文档、客户定单和客户安全类、返回存储过程的XML以及物理数据库层,此外还有一个(窗体)身份验证机制。
图1 Northwind架构
该应用程序所使用的数据存储区是示例Northwind数据库,在图1中有对相关表的概述。
此应用程序设计背后的推动力是表示层。已开发的解决方案的前提是使用HTML界面,显示来自Northwind数据库的客户、定单以及定单详细内容信息。这支持两种类型的用户——管理员和一般用户。此应用程序中的第一个步骤是用户身份验证,用户从这一步登录到系统中,这是通过.NET窗体身份验证机制完成的(此机制使用可记录用户登录信息的Http cookie)。其他本应使用的身份验证解决方案包括Microsoft Windows和Microsoft .NET Passport。
登录之后表示路径取决于用户类型。管理员被定义为有查看所有用户信息的权限。因此,管理员的主页是一个有客户存在的国家的列表,从列表中选择一个国家将显示相应的客户列表。一般用户被定义为有对预先定义的客户列表的访问权,因此,一般用户的主页是对于一般用户来说有权限的客户列表。一旦一个用户列表已经显示,HTML导航将显示一个客户详细信息页、一个客户定单列表、一个定单详细内容摘要页,以及一个完整的客户和定单摘要页。
表1 表示流:
页面描述操作XML存储过程样式表国家列表GetCustomersCountriesxml_customer_cty_listcustomercountry.xslt国家的客户GetCustomersByCountryxml_customer_ctycustomerlistcty.xslt用户的客户GetCustomersByUserxml_customer_usercustomerlistuser.xslt客户详细信息GetCustomerByIdxml_customer_idcustomerheader.xslt定单列表GetCustomerOrdersxml_customer_orderscustomerorders.xslt定单详细信息GetCustomerOrderDetailsxml_order_detailscustomerorderdetails.xslt客户与定单摘要GetCustomerSummByIdxml_customer_summarycustomersummary.xslt此应用程序通过使用返回XML的存储过程创建每个HTML页面,然后使用一个XSLT文档将其转换为HTML。表1概述了每个页面、支持XML检索的操作的名称、返回存储过程的关联XML的名称,最后是HTML从其生成的相应XSLT文档。
如前面所说的,这个应用程序使用SQL Server 2000 XML功能,表1中的每个操作都有一个相应的返回XML的存储过程。在设计这些存储过程时,需要考虑到结果XML的结构。如该应用程序所演示的,有数种技术可以使用。要直接从数据库检索XML结果,可以采用RAW、AUTO和EXPLICIT这三种模式之一,来使用SELECT语句的FOR XML子句。
RAW模式意味着结果XML中的每一行都有一般性的行标识符。AUTO模式在一个简单的嵌套XML树中返回查询结果。在SELECT语句中列出的FROM子句中的每个表以相同名称的一个XML元素表示,然后SELECT列被映射为这些元素的属性。层次结构的确定是基于由SELECT语句的列标识的表的顺序,要构建XML标识符列,应该使用别名。EXPLICIT模式指定XML树的形状,此查询指定所有信息以生成被称作通用树的东西。这意味着此查询除了指定所需要的数据外,还必须指定所有元数据。EXPLICIT模式的其他重要好处是列可以被分别映射到属性或子元素,而且它可以生成同辈层次结构。
在编写XML查询时,我们必须记住,XML标识符名称是区分大小写的,这在执行XSLT转换时很重要。此外,在XSLT文档内,我们还必须记住要用@符号做为XML标识符属性的前缀。
在XML的设计中所遇到的第一个挑战是,在XML层次结构需要修整时,用于单个层次结构层的信息派生自一个以上的单个表。要做到这一点,XML查询可以使用视图而非基础表,被看作单个表的视图修整结果XML层次结构。这是显示客户定单信息的一个要求,在这种情况下,需要用每个包含货主信息的定单元素来显示客户定单信息:
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] OrderDetsWHERE OrderDets.OrderID = Orders.OrderID) ProductCount,Orders.CompanyName ShipCompany, Orders.Phone ShipPhoneFROM Customers CustomerINNER JOIN dbo.view_orders OrdersON Customer.CustomerID = Orders.CustomerIDWHERE Customer.CustomerID = @custidFOR XML AUTO, ELEMENTS
查询中的视图是简单的内部联接查询:
SELECT Orders.*, Shippers.*FROM OrdersINNER JOIN Shippers ON Orders.ShipVia = Shippers.ShipperID
在生成的XML中,视图被视为一个单个的层次结构,即定单。
对于更复杂的XML结构的查询,可以使用FOR XML EXPLICIT子句。EXPLICIT模式被用于定单详细信息和客户定单摘要页。如前面所述,XML层次结构的结构是由一个生成的通用表确定的,因此视图的使用只与查询简化相关。以用于定单详细信息的查询为例:
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 CustomersINNER JOIN dbo.view_orders OrdersON Customers.CustomerID = Orders.CustomerIDWHERE Customers.CustomerID = @custidAND Orders.OrderID = @orderUNION ALLSELECT 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 OrdersINNER JOIN dbo.view_orderdetails OrderDetailsON Orders.OrderID = OrderDetails.OrderIDWHERE Orders.CustomerID = @custidAND Orders.OrderID = @orderORDER BY [CustomerOrder!1!CustomerId],[CustomerOrder!1!OrderId], [OrderDetails!2!ProductId]FOR XML EXPLICIT
使用EXPLICT模式时,主要任务之一是确保Tag和Parent属性的设置是正确的。因此,在这种情况下,使用UNION ALL来返回所需的客户以及之后的定单信息。最终的ORDER BY对正确地将定单与相应客户关联是很重要的。使用CAST函数是为了确保从查询返回正确的数据类型:数字型而非浮点型。如我们所能看到的,尽管EXPLICIT模式比较冗长,但它在结果通用树的生成中提供了更大的灵活性。要更好地了解通用表这一概念,请运行前一个不带FOR XML EXPLICIT子句的查询,其输出将会是通用表。
如在大部分应用程序中一样,对数据库的访问是通过一个数据访问层——CustomerOrders类来控制的。这个类的用途是对存储过程调用进行抽象化,并将XML显示给Http应用程序。选择的返回数据类型是一个XPathDocument,而非一个XmlDocument。这有双重原因:从SQL Server获得的XML是一个文档片段(可能没有一个根节点),因此不能直接加载到XmlDocument中;其次,XPathDocument为执行转换提供最佳性能。
每个带有该类的方法调用的结构都是相同的:格式化一个SQLCommand及其参数,设置其活动连接,执行XML阅读器,并返回构造化的XPath文档。由于只有SQLCommand设置区分数据访问方法调用,因此XML处理是由一个私有方法处理的:
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;}
SqlCommand类的ExecuteXmlReader方法返回一个XmlReader以表示返回的XML,这个阅读器被用来构造XPath文档。在进行这个方法调用前,该调用方法将创建一个新的SqlCommand,指定要执行哪个存储过程并定义相应的参数集合。
如我们从表1中看到的,类方法与一个返回XML的存储过程以及一个表示Web应用程序的函数一一对应。图2概述CustomerOrders类的公共方法和私有方法,类方法都是容易获得的方法,并返回所需XML的XPath文档表示形式。
图2 CustomerOrders类图
类方法的唯一工作是定义存储过程及相关联的参数,不执行任何身份验证检查,该操作是在前面的一个步骤中完成的。请考虑此方法,以返回一个客户定单的详细信息:
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);}
如我们所看到的,所有XML工作都在CommandToXPath方法中完成。
这里,有对于安全实现的一个保证。正如已经提到过的,CustomerOrders类对于安全不做任何假定,那是交由Http来做的。安全机制是通过数据库实体、存储过程、一个安全类以及ASP.NET窗体身份验证机制一起来支持的。
表2 安全性操作:
说明操作对用户进行身份验证。在登录页面上使用,用来验证用户名称和密码。ValidateUserLogin获取用户信息。在默认页面上使用,用来显示用户名称和其他信息。GetUserInfo对用户进行关于客户的授权。对于一般用户请求,验证对被请求客户的读访问权。ValidateUserCustomer获取一个管理员列表。在Http应用程序内,用于确定已验证身份的用户是否为管理员。GetAdminUsers支持身份验证和数据授权所需的基本操作已在表2中列出。
在整个应用程序中使用的数据模型是Northwind数据库的数据模型。但是,对于将用户映射到客户并支持身份验证和授权的要求,就需要扩展了。为了隔离安全实现,我们开发了一个名为NWSecurity的单独数据库,它只包含两个表:第一表是用户表,其中用一个字节标志标记管理员:
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))
第二个表是用户客户表,用于定义用户分配。对于一个有效的授权检查,用户或者是一个管理员,或者被允许访问该用户:
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 CASCADEON DELETE CASCADE)
NWSecurity数据库包含若干支持安全性操作的存储过程:对一个用户访问客户信息的验证,对一个登录用户的名称和密码的验证,用户信息的检索和管理员列表的检索。表3列出了这些基于调用类方法的存储过程:
表3 安全存储过程:
类方法存储过程ValidateUserLoginusp_validate_user_loginGetUserInfousp_get_user_infoValidateUserCustomerusp_validate_user_customer_readGetAdminUsersusp_admin_users在存储过程的设计中,我们做出了这样的假定:系统的大部分用户将是一般用户。由于这个原因,会执行表NWCustomer而非NWUser的读取,用于管理员检查。我们考虑对一般用户的每个请求前要有身份验证检查时,这是很重要的。
同样的,在用应用程序数据访问层时,安全类的方法与一个存储过程一一对应。如图3中概述的,一个CustomerSecurity类是作为应用程序和物理数据之间的一个抽象层。
图3 CustomerSecurity类图
如果出现ValidateUserLogin,则返回值是一个枚举:
public enum LoginReturnCode{ NoAccess = 0, AccessIncorrectPassword = 1, AccessAuthenticated = 2}
枚举的值与从相应存储过程返回的值相匹配。因此在这种情况下,非查询存储过程返回值可以被转换为枚举类型:
securityCom.ExecuteNonQuery(); LoginReturnCode lrcValue = (LoginReturnCode)returnParam.Value;
GetAdminUsers方法与其他方法的不同之处在于,它返回一个简单的管理员名称数组:
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;}
使用SqlConnection和SqlCommand(当对象超出范围时将被处置),会构造一个SqlDataReader,然后会分析数据读取器以获取用户名(其值被放置在一个数组列表中)列表。然后这个数组列表被简化为一个包含用户名的字符串变量数组。
身份验证背后的原则是对用户进行验证以访问到系统中。对于所有用户,无论是管理员还是一般用户,都依照用户数据库表执行身份验证。要强制进行身份验证,需要执行一个自定义窗体实现。在Web应用程序Web.config文件内,修改了身份验证和授权部分以强制进行窗体身份验证,并拒绝对匿名用户的访问,其他受支持的机制是Windows和Passport身份验证:
<authentication mode="Forms"> <forms name="CustomerServiceApp" loginUrl="login.aspx" protection="All" timeout="30" path="/" /></authentication><authorization> <deny users="?" /></authorization>
下一个任务是编写login.aspx页面来依照数据库对用户进行验证。这一页面的用途是允许用户输入一个用户名称和密码,依照数据库对其进行验证,为其后的调用在cookie内保持一个用户标记,最后将用户定向回最初请求的页面——.NET Framework使所有这些任务简单得令人吃惊。
页面的UI非常简单,有两个文本框:一个用于用户名称,另一个用于密码;一个复选框,使用户决定是否将在浏览器会话之间保持身份验证;最后是一个登录按钮;它们都是带有一个服务器RUNAT标记的标准HTML标记,简单的服务器端Web窗口控件用于确保在用户名和密码字段输入了值。另外,一个客户端标签用于给用户反馈。
所有身份验证的实际工作都是以服务器端代码执行的。发布页面时,输入的凭据是用CustomerSecurity类的ValidateUserLogin方法验证的:
// get required query string parametersstring userName = UserName.Value;string userPassword = UserPassword.Value;// create a customer security object and make call to validate the userCustomerSecurity customerSecurity = new CustomerSecurity();CustomerSecurity.LoginReturnCode loginReturn = customerSecurity.ValidateUserLogin(userName, userPassword);
这里,UserName和UserPassword是初始化的服务器端控件。如果接收到正确的登录返回代码,则用户通过身份验证并被重定向到请求的页面:
if (loginReturn == CustomerSecurity.LoginReturnCode.AccessAuthenticated){ FormsAuthentication.RedirectFromLoginPage (UserName.Value, PersistForms.Checked);}
在颁发身份验证票据后,静态RedirectFromLoginPage方法将用户重定向到最初请求的页面。第二个参数接受一个布尔值,指定是否发出一个持久的cookie。这是从PersistForms HTML复选框派生的。
身份验证机制的最后一个重要部分是确定以何种角色安置已验证身份的用户。在应用程序global.asax文件中,包括一个允许我们从原始用户标识重新定义用户主体的一个身份验证事件。此时,可以定义用户角色。
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; } }}
用户角色被定义为管理员或一般用户。GetAdminUsers方法缓存并返回一个管理员数组,下面有关于此的更多内容。
Http应用程序利用ASP.NET Http运行时支持——一种对ISAPI扩展和筛选器API的逻辑替换,给予我们一种与低级别请求和Microsoft IIS Web服务器的响应服务交互的方法。下面是应用程序内使用的主要接口:
• IHttpHandler:被实现以处理同步Http请求。必须实现ProcessRequest方法以提供自定义URL执行。• IHttpAsyncHandler:被实现以处理异步Http请求。ProcessRequest方法是通过BeginProcessRequest和EndProcessRequest方法实现的。• IHttpHandlerFactory:被实现以创建新的IHttpHandler对象。唯一用途是动态生成实现IHttpHandler接口的新处理程序对象。
一个IHttpHandlerFactory实现通过查看已验证身份的用户处理每个请求。对于管理员,返回一个名为CustomerAdmin的IHttpHandler实现;对一般用户,返回一个名为CustomerGeneric的IHttpAsyncHandler实现。
如图4中所述,为了支持处理程序,我们使用了CustomerRequest类,这个类有一个名为ProcessRequest的公开的方法。使用提供的HttpContext,它将检索Http请求并输出所需要的HTML。这个类又使用CustomerOrders和CustomerSecurity类来进行数据访问和安全检查。
图4 CustomerRequest类关系图
在一个单个Http处理程序应用程序正在处理所有请求时,相应的函数和呈现的HTML被一个在URL查询字符串中传递的Function参数确定,当函数没有出现时会显示相应用户的主页。表4概述这些函数代码以及支持CustomerOrders方法:
表4 应用程序函数代码:
管理员函数一般用户函数类方法Null GetCustomersCountriesCC GetCustomersByCountry nullGetCustomersByUserCHCHGetCustomerByIdOSOSGetCustomerOrdersODODGetCustomerOrderDetailsCSCSGetCustomerSummById基于这个函数参数,调用适当的类方法,该类方法返回数据的XML表示形式。然后使用XSLT文档使其转换为HTML,并传递到Http响应流中。
对于一个管理员,返回一个实现IHttpHandler的CustomerAdmin处理程序实例。对于一个一般用户,返回一个实现IHttpHandlerAsync的CustomerGeneric处理程序。那么如何确定一个管理员?基于角色的编程的概念就是在这里起作用的,我们只需查看以前定义的用户角色:
if (context.User.IsInRole("Admin")) >= 0){ return (new CustomerAdmin());}else{ return (new CustomerGeneric());}
在概述处理程序实现之前,让我们讨论一下IHttpHandler接口:
public interface IHttpHandler{ void ProcessRequest(HttpContext context); bool IsReusable {get;}}
为每个Http请求都调用单个方法ProcessRequest。给定的HttpContext对象给用于为Http请求(如请求、响应、会话和服务器)服务的内部服务器对象提供引用。IsReusable属性指示IHttpHandler实例是否是可以重新使用的。在所有两个给定的实现中,处理程序将始终被认为是可重新使用的,除非应用程序发生异常。我们定义了一个ApplicationException派生类,用于异常处理。新的Exception类旨在提供一个属性——Terminated,处理程序实现使用它来确定处理程序是否保留在池中。一个真值指示一个处理异常,例如应该将处理程序对象从处理池移除。
CustomerAdmin——对管理员的IHttpHandler实现,在下面列出:
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); } }}
ProcessRequest创建CustomerRequest类的一个实例,调用一个相应的ProcessRequest方法来处理Http请求。如果CustomerRequest对象引发一个CustomerRequestException,应用程序可以选择决定应用程序是否应保留在异常池中。相对而言,一般用户的处理程序实现可以实现IHttpAsyncHandler接口。这个接口有一个BeginProcessRequest方法,此方法用来初始化对Http处理程序的一个异步调用。对于一般用户请求,一个单独的过程执行授权检查。由于这个检查是异步执行的,因此从一般用户实现演示异步处理程序是有意义的。为执行异步调用,我们为ProcessRequest方法定义了一个委托。生成了BeginInvoke和EndInvoke方法的编译器之后被用于异步调用一个CustomerRequest实例的ProcessRequest方法:
internal delegate void ProcessRequestDelegate(HttpContext context);// start the processing of the async HTTP requestpublic 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 completioninternal 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); }}
如我们看到的,Http请求正以两个部分被处理。BeginProcessRequest通过委托BeginInvoke方法初始化异步调用,在这一阶段的错误是基础结构类型的。通过委托和回调对象,EndInvoke在异步处理完成时被调用以获取返回数据(包括异常)。通过名为ProcessAsyncResult的IAsyncResult接口的实现,使调用类了解Http处理程序的状态。
Http处理程序的作用S是管理一个执行HTML呈现和授权检查的CustomerRequest对象。它使用CustomerOrders类来获取XML信息,并用适当的XSLT文档将这个信息转换到HTML中,它使用了一个单个的公开的方法——ProcessRequest:
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); }}
这个方法使用两个三态值——validProcess和validSecurity,用来表明处理的状态:0表明所有都是有效的,1表明没有引发错误但不能处理请求,2表明遇到了处理错误。
在私有ReturnCustomerXML方法中,适当的CustomerOrders方法被调用,一个XPathDocument和已加载的XslTransform做为输出。然后XslTransform的Transform方法将HTML输出到响应流:
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;}
实现ReturnCustomerXml方法的前提是简单的:使用请求的函数代码调用相应的CustomerOrders方法获取XPathDocument,并从派生的XSLT文件名加载所需的XslTransform。为了加快处理,XSLT文档被预先加载并保存在应用程序缓存中。在这个类中,我们将注意到WriteTraceOutput方法,其用途是将信息写入跟踪文件,这提供了一个来通过trace.axd页查看关于处理异常的信息性消息的机制。
授权与身份验证的不同之处在于,它验证所有请求,不是为了访问应用程序,而是为了确保用户被限制在一个已定义的数据子集。这是通过将一个用户限制在一组分配好的客户群中(如NWCustomer表中定义的)来完成的。对于一般用户,使用一个专用处理线程来通过ValidateCustomerSecurity方法执行授权检查。检查的有效性是由适当的三态值表示的:
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;}
在处理Http请求时,安全线程被启动,并随后用Join方法将其与处理线程进行联接,此时要在呈现HTML前验证安全状态。
我们已经提到过两次,静态数据已经为一群管理员和已加载的XSLT文档被缓存在应用程序内。对于每个请求,当将数据进行缓存而非进行处理时,您会发现性能有显著提高。当前的HttpContext管理对一个应用程序缓存对象的引用,在这个对象内缓存的数据可能被不同的机制(包括定义的时间范围、绝对时间和文件更改通知事件)标记为无效。
身份验证事件将上下文缓存用于管理员数组——这个数组是通过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;}
我们还应注意到,除了一个20分钟的截止时间外,此缓存还在XML文件上有一个依赖项。代码包含一个将生成这个文件的脚本,我们可以将此用作一个机制,以便在更改管理员列表时刷新缓存。
以一种非常相似的方式,CustomerOrders类有一个从缓存返回已加载的XSLT文档的GetStyleSheet方法。在这种情况下,代码与管理员数组功能十分相似。最初缓存对象被获取并被转换为一个XslTransform对象:
XslTransform docStyle = (XslTransform)context.Cache[styleName];
如果缓存为空、从未加载或无效,则加载适当的XSLT文档并将其放置到缓存中。这种情况下的缓存仅依赖于XslTransform从其加载的文件:
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));}
使用上下文缓存的另一个选择是缓存汇集的处理程序内的所有适当数据或应用程序范围变量。上下文缓存的好处是预先定义的机制,通过这些机制,缓存可以被标记为无效,从而被自动重新加载。除了上下文缓存,应用程序还可以应用Http处理程序被汇集这一事实。为了支持这一点,每个处理程序构造函数创建CustomerRequest类的一个实例,后者又创建CustomerOrders和CustomerSecurity类的一个实例。另外,CustomerOrders类创建一个用于授权检查的线程对象。在使用这个保存对象引用的方法的中,每个由ProcessRequest方法处理的请求都是无状态的,每个Http处理程序请求必须初始化所有本地处理变量。另外,如果遇到错误,Http处理程序被标记为非重新使用的,从而强制生成一个新的处理程序以及所有关联对象。
CustomerOrders和CustomerSecurity类不保存数据库连接,而只是保存从配置文件读取的连接字符串,这使得OLEDB资源池能够管理数据库连接。
到目前为止,此应用程序是在这样的前提下工作的:使用者将使用Web应用程序来访问客户和定单信息。随着.NET Framework Web服务基础结构的出现,另一个解决方案看起来是合理的:允许从Web服务方法返回XML,从而使使用者能够将应用程序集成到客户自己的系统中。
实现Web服务的前提很简单:公开返回客户和定单信息的核心类方法。图5概述用以支持这个要求的CustomerWebService类定义:
图5 Web服务类图
每个类方法都使用CustomerOrder类以获取结果XML(其格式为一个XPathDocument),并将此作为一个调用方可以按需要使用的XmlElement返回。始终假定用户是已验证身份且已知的,对此,后面有更多内容进行介绍。以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);}
SoapHeader属性和ValidateCustomerSecurity都处理安全性,对用户进行身份验证,并为他们设置客户数据授权。GetElement方法从CustomerOrders类得到XPathDocument,并将其转换到所需的XmlElement中:
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;}
这个方法使用一个XSLT文档获取一个XpathDocument,并执行一个简单的转换:
<?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>
这样做的目的只是要获取XpathDocument的字符串表示形式——可以从其获取一个XmlDocument及其根元素,这一转换假定XPath文档片段有一个根节点。如果情况并非如此,就需要将XSLT文档重新构造来包含一个。
如您看到的,Web服务的实际实现是很简捷的,但是安全性怎么样呢?在Web服务内处理身份验证和授权有很多种选择,在上面的实现中,使用了自定义SOAP头。对于自定义SOAP头,一个解决方案是一个通过SOAP头对用户进行身份验证的HttpModule。我选择的技术略有不同:我定义了两个SOAP头作为Web服务的部分。可选SOAPCredentials头用于在依照用户数据库验证的用户凭证中传递。当身份验证成功时,会创建一个授权标记,它是带有截止时间的用户名称的加密版本,并被放置在所要求的SOAPTicket头中。就是这个在后面的Web方法调用时加密并验证的票据,节约了一次到数据库以执行身份验证的往返过程。
希望这里所展示的解决方案阐明了Microsoft .NET Framework在编写使用SQL Server 2000的XML功能的应用程序上的强大特性。有了ASP.NET运行时支持、窗体身份验证机制,以及系统XML类,极大简化了高性能且可伸缩的基于XML的查询系统的创建。另外,同时存在包含Web服务基础结构的新的可能性。以前公开的基于Web的应用程序可以很容易地被公开为Web服务,这使得使用者能够轻易将所提供的功能集成到其自己的应用程序中。
Carl Nolan在位于北加州硅谷的Microsoft技术中心工作,这个中心致力于使用Microsoft Windows .NET平台开发Microsoft .NET解决方案。
